├── .ddev
├── config.yaml
└── providers
│ ├── acquia.yaml
│ ├── lagoon.yaml
│ └── upsun.yaml
├── .github
└── ISSUE_TEMPLATE
│ ├── BUG-REPORT.yml
│ ├── FEATURE-REQUEST.yml
│ ├── QUESTION.yml
│ └── config.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── composer.lock
├── config
└── tools.php
├── docs
└── assets
│ ├── author-instructions-example.png
│ ├── width-field.png
│ └── width-settings.png
├── ecs.php
└── src
├── Tools.php
├── assetbundles
└── tools
│ ├── ToolsAsset.php
│ └── dist
│ ├── css
│ ├── nouislider.css
│ ├── nouislider.pips.css
│ └── tools.css
│ └── js
│ ├── nouislider.js
│ └── tools.js
├── controllers
└── ToolsController.php
├── fields
├── Ancestors.php
├── AuthorInstructions.php
├── CategoriesMultipleGroups.php
├── CategoriesSearch.php
├── DisabledCategories.php
├── DisabledDropdown.php
├── DisabledEntries.php
├── DisabledLightswitch.php
├── DisabledNumber.php
├── DisabledPlainText.php
├── DisabledProducts.php
├── DisabledUsers.php
├── DisabledVariants.php
├── EntriesSearch.php
├── Grid.php
├── ProductsSearch.php
├── VariantsSearch.php
├── Width.php
└── data
│ ├── GridData.php
│ └── WidthData.php
├── icon.svg
├── models
└── Settings.php
├── templates
├── _components
│ ├── fields
│ │ ├── ancestors
│ │ │ └── settings.twig
│ │ ├── authorinstructions
│ │ │ ├── input.twig
│ │ │ └── settings.twig
│ │ ├── categoriessearch
│ │ │ └── input.twig
│ │ ├── disabledplaintext
│ │ │ └── settings.twig
│ │ ├── entriessearch
│ │ │ └── input.twig
│ │ ├── grid
│ │ │ ├── input.twig
│ │ │ └── settings.twig
│ │ ├── productssearch
│ │ │ └── input.twig
│ │ ├── variantssearch
│ │ │ └── input.twig
│ │ └── width
│ │ │ └── input.twig
│ └── widgets
│ │ └── rollyourown
│ │ └── settings.twig
└── _includes
│ └── elementssearch.twig
└── widgets
└── RollYourOwn.php
/.ddev/config.yaml:
--------------------------------------------------------------------------------
1 | name: craft-odds-and-ends
2 | type: php
3 | docroot: ""
4 | php_version: "8.2"
5 | webserver_type: nginx-fpm
6 | xdebug_enabled: false
7 | additional_hostnames: []
8 | additional_fqdns: []
9 | database:
10 | type: mysql
11 | version: "8.0"
12 | use_dns_when_possible: true
13 | composer_version: "2"
14 | web_environment: []
15 | nodejs_version: "18"
16 |
17 | # Key features of DDEV's config.yaml:
18 |
19 | # name: # Name of the project, automatically provides
20 | # http://projectname.ddev.site and https://projectname.ddev.site
21 |
22 | # type: # drupal6/7/8, backdrop, typo3, wordpress, php
23 |
24 | # docroot: # Relative path to the directory containing index.php.
25 |
26 | # php_version: "8.1" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3"
27 |
28 | # You can explicitly specify the webimage but this
29 | # is not recommended, as the images are often closely tied to DDEV's' behavior,
30 | # so this can break upgrades.
31 |
32 | # webimage: # nginx/php docker image.
33 |
34 | # database:
35 | # type: # mysql, mariadb, postgres
36 | # version: # database version, like "10.4" or "8.0"
37 | # MariaDB versions can be 5.5-10.8 and 10.11, MySQL versions can be 5.5-8.0
38 | # PostgreSQL versions can be 9-15.
39 |
40 | # router_http_port: # Port to be used for http (defaults to global configuration, usually 80)
41 | # router_https_port: # Port for https (defaults to global configuration, usually 443)
42 |
43 | # xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart"
44 | # Note that for most people the commands
45 | # "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better,
46 | # as leaving Xdebug enabled all the time is a big performance hit.
47 |
48 | # xhprof_enabled: false # Set to true to enable Xhprof and "ddev start" or "ddev restart"
49 | # Note that for most people the commands
50 | # "ddev xhprof" to enable Xhprof and "ddev xhprof off" to disable it work better,
51 | # as leaving Xhprof enabled all the time is a big performance hit.
52 |
53 | # webserver_type: nginx-fpm, apache-fpm, or nginx-gunicorn
54 |
55 | # timezone: Europe/Berlin
56 | # This is the timezone used in the containers and by PHP;
57 | # it can be set to any valid timezone,
58 | # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
59 | # For example Europe/Dublin or MST7MDT
60 |
61 | # composer_root:
62 | # Relative path to the Composer root directory from the project root. This is
63 | # the directory which contains the composer.json and where all Composer related
64 | # commands are executed.
65 |
66 | # composer_version: "2"
67 | # You can set it to "" or "2" (default) for Composer v2 or "1" for Composer v1
68 | # to use the latest major version available at the time your container is built.
69 | # It is also possible to use each other Composer version channel. This includes:
70 | # - 2.2 (latest Composer LTS version)
71 | # - stable
72 | # - preview
73 | # - snapshot
74 | # Alternatively, an explicit Composer version may be specified, for example "2.2.18".
75 | # To reinstall Composer after the image was built, run "ddev debug refresh".
76 |
77 | # nodejs_version: "18"
78 | # change from the default system Node.js version to another supported version, like 14, 16, 18, 20.
79 | # Note that you can use 'ddev nvm' or nvm inside the web container to provide nearly any
80 | # Node.js version, including v6, etc.
81 |
82 | # additional_hostnames:
83 | # - somename
84 | # - someothername
85 | # would provide http and https URLs for "somename.ddev.site"
86 | # and "someothername.ddev.site".
87 |
88 | # additional_fqdns:
89 | # - example.com
90 | # - sub1.example.com
91 | # would provide http and https URLs for "example.com" and "sub1.example.com"
92 | # Please take care with this because it can cause great confusion.
93 |
94 | # upload_dirs: "custom/upload/dir"
95 | #
96 | # upload_dirs:
97 | # - custom/upload/dir
98 | # - ../private
99 | #
100 | # would set the destination paths for ddev import-files to /custom/upload/dir
101 | # When Mutagen is enabled this path is bind-mounted so that all the files
102 | # in the upload_dirs don't have to be synced into Mutagen.
103 |
104 | # disable_upload_dirs_warning: false
105 | # If true, turns off the normal warning that says
106 | # "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set"
107 |
108 | # working_dir:
109 | # web: /var/www/html
110 | # db: /home
111 | # would set the default working directory for the web and db services.
112 | # These values specify the destination directory for ddev ssh and the
113 | # directory in which commands passed into ddev exec are run.
114 |
115 | # omit_containers: [db, ddev-ssh-agent]
116 | # Currently only these containers are supported. Some containers can also be
117 | # omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit
118 | # the "db" container, several standard features of DDEV that access the
119 | # database container will be unusable. In the global configuration it is also
120 | # possible to omit ddev-router, but not here.
121 |
122 | # performance_mode: "global"
123 | # DDEV offers performance optimization strategies to improve the filesystem
124 | # performance depending on your host system. Should be configured globally.
125 | #
126 | # If set, will override the global config. Possible values are:
127 | # - "global": uses the value from the global config.
128 | # - "none": disables performance optimization for this project.
129 | # - "mutagen": enables Mutagen for this project.
130 | # - "nfs": enables NFS for this project.
131 | #
132 | # See https://ddev.readthedocs.io/en/latest/users/install/performance/#nfs
133 | # See https://ddev.readthedocs.io/en/latest/users/install/performance/#mutagen
134 |
135 | # fail_on_hook_fail: False
136 | # Decide whether 'ddev start' should be interrupted by a failing hook
137 |
138 | # host_https_port: "59002"
139 | # The host port binding for https can be explicitly specified. It is
140 | # dynamic unless otherwise specified.
141 | # This is not used by most people, most people use the *router* instead
142 | # of the localhost port.
143 |
144 | # host_webserver_port: "59001"
145 | # The host port binding for the ddev-webserver can be explicitly specified. It is
146 | # dynamic unless otherwise specified.
147 | # This is not used by most people, most people use the *router* instead
148 | # of the localhost port.
149 |
150 | # host_db_port: "59002"
151 | # The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic
152 | # unless explicitly specified.
153 |
154 | # mailpit_http_port: "8025"
155 | # mailpit_https_port: "8026"
156 | # The Mailpit ports can be changed from the default 8025 and 8026
157 |
158 | # host_mailpit_port: "8025"
159 | # The mailpit port is not normally bound on the host at all, instead being routed
160 | # through ddev-router, but it can be bound directly to localhost if specified here.
161 |
162 | # webimage_extra_packages: [php7.4-tidy, php-bcmath]
163 | # Extra Debian packages that are needed in the webimage can be added here
164 |
165 | # dbimage_extra_packages: [telnet,netcat]
166 | # Extra Debian packages that are needed in the dbimage can be added here
167 |
168 | # use_dns_when_possible: true
169 | # If the host has internet access and the domain configured can
170 | # successfully be looked up, DNS will be used for hostname resolution
171 | # instead of editing /etc/hosts
172 | # Defaults to true
173 |
174 | # project_tld: ddev.site
175 | # The top-level domain used for project URLs
176 | # The default "ddev.site" allows DNS lookup via a wildcard
177 | # If you prefer you can change this to "ddev.local" to preserve
178 | # pre-v1.9 behavior.
179 |
180 | # ngrok_args: --basic-auth username:pass1234
181 | # Provide extra flags to the "ngrok http" command, see
182 | # https://ngrok.com/docs/ngrok-agent/config or run "ngrok http -h"
183 |
184 | # disable_settings_management: false
185 | # If true, DDEV will not create CMS-specific settings files like
186 | # Drupal's settings.php/settings.ddev.php or TYPO3's AdditionalConfiguration.php
187 | # In this case the user must provide all such settings.
188 |
189 | # You can inject environment variables into the web container with:
190 | # web_environment:
191 | # - SOMEENV=somevalue
192 | # - SOMEOTHERENV=someothervalue
193 |
194 | # no_project_mount: false
195 | # (Experimental) If true, DDEV will not mount the project into the web container;
196 | # the user is responsible for mounting it manually or via a script.
197 | # This is to enable experimentation with alternate file mounting strategies.
198 | # For advanced users only!
199 |
200 | # bind_all_interfaces: false
201 | # If true, host ports will be bound on all network interfaces,
202 | # not the localhost interface only. This means that ports
203 | # will be available on the local network if the host firewall
204 | # allows it.
205 |
206 | # default_container_timeout: 120
207 | # The default time that DDEV waits for all containers to become ready can be increased from
208 | # the default 120. This helps in importing huge databases, for example.
209 |
210 | #web_extra_exposed_ports:
211 | #- name: nodejs
212 | # container_port: 3000
213 | # http_port: 2999
214 | # https_port: 3000
215 | #- name: something
216 | # container_port: 4000
217 | # https_port: 4000
218 | # http_port: 3999
219 | # Allows a set of extra ports to be exposed via ddev-router
220 | # Fill in all three fields even if you don’t intend to use the https_port!
221 | # If you don’t add https_port, then it defaults to 0 and ddev-router will fail to start.
222 | #
223 | # The port behavior on the ddev-webserver must be arranged separately, for example
224 | # using web_extra_daemons.
225 | # For example, with a web app on port 3000 inside the container, this config would
226 | # expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998
227 | # web_extra_exposed_ports:
228 | # - name: myapp
229 | # container_port: 3000
230 | # http_port: 9998
231 | # https_port: 9999
232 |
233 | #web_extra_daemons:
234 | #- name: "http-1"
235 | # command: "/var/www/html/node_modules/.bin/http-server -p 3000"
236 | # directory: /var/www/html
237 | #- name: "http-2"
238 | # command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000"
239 | # directory: /var/www/html
240 |
241 | # override_config: false
242 | # By default, config.*.yaml files are *merged* into the configuration
243 | # But this means that some things can't be overridden
244 | # For example, if you have 'nfs_mount_enabled: true'' you can't override it with a merge
245 | # and you can't erase existing hooks or all environment variables.
246 | # However, with "override_config: true" in a particular config.*.yaml file,
247 | # 'nfs_mount_enabled: false' can override the existing values, and
248 | # hooks:
249 | # post-start: []
250 | # or
251 | # web_environment: []
252 | # or
253 | # additional_hostnames: []
254 | # can have their intended affect. 'override_config' affects only behavior of the
255 | # config.*.yaml file it exists in.
256 |
257 | # Many DDEV commands can be extended to run tasks before or after the
258 | # DDEV command is executed, for example "post-start", "post-import-db",
259 | # "pre-composer", "post-composer"
260 | # See https://ddev.readthedocs.io/en/stable/users/extend/custom-commands/ for more
261 | # information on the commands that can be extended and the tasks you can define
262 | # for them. Example:
263 | #hooks:
264 |
--------------------------------------------------------------------------------
/.ddev/providers/acquia.yaml:
--------------------------------------------------------------------------------
1 | #ddev-generated
2 | # Acquia provider configuration.
3 |
4 | # To use this configuration,
5 |
6 | # 1. Get your Acquia API token from your Account Settings->API Tokens.
7 | # 2. Make sure your ssh key is authorized on your Acquia account at Account Settings->SSH Keys
8 | # 3. `ddev auth ssh` (this typically needs only be done once per ddev session, not every pull).
9 | # 4. Add / update the web_environment section in ~/.ddev/global_config.yaml
10 | # or your project config.yamlwith the API keys:
11 | # ```yaml
12 | # web_environment:
13 | # - ACQUIA_API_KEY=xxxxxxxx
14 | # - ACQUIA_API_SECRET=xxxxx
15 | # ```
16 | # 5. Add the ACQUIA_ENVIRONMENT_ID environment variable to your project config.yaml, for example:
17 | # ```yaml
18 | # web_environment:
19 | # - ACQUIA_ENVIRONMENT_ID=project1.dev
20 | # - On the Acquia Cloud Platform you can find this out by navigating to the environments page,
21 | # clicking on the header and look for the "SSH URL" line.
22 | # Eg. `project1.dev@cool-projects.acquia-sites.com` would have a project ID of `project1.dev`
23 | # 6. `ddev restart`
24 | # 7. Use `ddev pull acquia` to pull the project database and files.
25 | # 8. Optionally use `ddev push acquia` to push local files and database to Acquia. Note that `ddev push` is a command that can potentially damage your production site, so this is not recommended.
26 |
27 | # Debugging: Use `ddev exec acli command` and `ddev exec acli auth:login`
28 |
29 | # Instead of setting the environment variables in configuration files, you can use
30 | # `ddev pull acquia --environment=ACQUIA_ENVIRONMENT_ID=yourproject.dev` for example
31 |
32 | auth_command:
33 | command: |
34 | set -eu -o pipefail
35 | if [ -z "${ACQUIA_API_KEY:-}" ] || [ -z "${ACQUIA_API_SECRET:-}" ]; then echo "Please make sure you have set ACQUIA_API_KEY and ACQUIA_API_SECRET in ~/.ddev/global_config.yaml" && exit 1; fi
36 | if [ -z "${ACQUIA_ENVIRONMENT_ID:-}" ] ; then echo "Please set ACQUIA_ENVIRONMENT_ID via config.yaml or with '--environment=ACQUIA_ENVIRONMENT_ID=xxx'" && exit 1; fi
37 | ssh-add -l >/dev/null || ( echo "Please 'ddev auth ssh' before running this command." && exit 1 )
38 | acli -n auth:login -n --key="${ACQUIA_API_KEY}" --secret="${ACQUIA_API_SECRET}"
39 |
40 | db_pull_command:
41 | command: |
42 | set -eu -o pipefail
43 | # xargs here just trims whitespace
44 | # We could use an easier technique when https://github.com/acquia/cli/issues/1629 is resolved
45 | # just using `acli pull:db ${ACQUIA_ENVIRONMENT_ID}`
46 | echo "Using ACQUIA_ENVIRONMENT_ID=${ACQUIA_ENVIRONMENT_ID}"
47 | set -x # You can enable bash debugging output by uncommenting
48 | db_dump=$(acli pull:db ${ACQUIA_ENVIRONMENT_ID} --no-interaction --no-import | tail -2l | xargs | sed 's/^.* //')
49 | ls /var/www/html/.ddev >/dev/null # This just refreshes stale NFS if possible
50 | cp ${db_dump} /var/www/html/.ddev/.downloads/db.sql.gz
51 |
52 | files_import_command:
53 | command: |
54 | # set -x # You can enable bash debugging output by uncommenting
55 | set -eu -o pipefail
56 | acli -n pull:files ${ACQUIA_ENVIRONMENT_ID}
57 |
58 | # push is a dangerous command. If not absolutely needed it's better to delete these lines.
59 | db_push_command:
60 | command: |
61 | set -eu -o pipefail
62 | export ACLI_DB_HOST=db ACLI_DB_NAME=db ACLI_DB_USER=db ACLI_DB_PASSWORD=db
63 | set -x # You can enable bash debugging output by uncommenting
64 | acli push:db ${ACQUIA_ENVIRONMENT_ID} --no-interaction
65 |
66 | # push is a dangerous command. If not absolutely needed it's better to delete these lines.
67 | files_push_command:
68 | command: |
69 | # set -x # You can enable bash debugging output by uncommenting
70 | set -eu -o pipefail
71 | acli push:files ${ACQUIA_ENVIRONMENT_ID} --no-interaction
72 |
--------------------------------------------------------------------------------
/.ddev/providers/lagoon.yaml:
--------------------------------------------------------------------------------
1 | #ddev-generated
2 | # Lagoon provider configuration.
3 |
4 | # To use this configuration,
5 |
6 | # 1. Check out the project and then configure it with 'ddev config'. You'll want to use 'ddev start' and make sure the basic functionality is working. Your project must have a .lagoon.yml properly configured.
7 | # 2. Configure an SSH key for your Lagoon user https://docs.lagoon.sh/using-lagoon-advanced/ssh/
8 | # 3. `ddev auth ssh`.
9 | # 4. Add LAGOON_PROJECT and LAGOON_ENVIRONMENT variables to your project in 'web_environment' or a '.ddev/.env'
10 | # 5. `ddev restart`
11 | #
12 | # 'ddev pull lagoon'
13 |
14 | auth_command:
15 | command: |
16 | set -eu -o pipefail
17 | ssh-add -l >/dev/null || ( echo "Please 'ddev auth ssh' before running this command." && exit 1 )
18 | if [ -z "${LAGOON_PROJECT:-}" ]; then echo "Please make sure you have set the LAGOON_PROJECT environment variable in your 'web_environment' or .ddev/.env." && exit 1; fi
19 | if [ -z "${LAGOON_ENVIRONMENT}" ]; then echo "Please make sure you have set the LAGOON_ENVIRONMENT environment variable in your 'web_environment' or .ddev/.env." && exit 1; fi
20 |
21 |
22 | db_import_command:
23 | command: |
24 | # set -x # You can enable bash debugging output by uncommenting
25 | set -eu -o pipefail
26 | export MARIADB_HOST=db MARIADB_USERNAME=db MARIADB_PASSWORD=db MARIADB_DATABASE=db
27 | lagoon-sync sync mariadb -p ${LAGOON_PROJECT} -e ${LAGOON_ENVIRONMENT} --no-interaction
28 |
29 | files_import_command:
30 | command: |
31 | #set -x # You can enable bash debugging output by uncommenting
32 | set -eu -o pipefail
33 | lagoon-sync sync files -p ${LAGOON_PROJECT} -e ${LAGOON_ENVIRONMENT} --no-interaction
34 |
35 | # push is a dangerous command. If not absolutely needed it's better to delete these lines.
36 | db_push_command:
37 | command: |
38 | set -eu -o pipefail
39 | #set -x # You can enable bash debugging output by uncommenting
40 | export MARIADB_HOST=db MARIADB_USERNAME=db MARIADB_PASSWORD=db MARIADB_DATABASE=db
41 | lagoon-sync sync mariadb -p ${LAGOON_PROJECT} -t ${LAGOON_ENVIRONMENT} -e local --no-interaction
42 |
43 | # push is a dangerous command. If not absolutely needed it's better to delete these lines.
44 | files_push_command:
45 | command: |
46 | set -eu -o pipefail
47 | #set -x # You can enable bash debugging output by uncommenting
48 | lagoon-sync sync files -p ${LAGOON_PROJECT} -e local -t ${LAGOON_ENVIRONMENT} --no-interaction
49 |
50 |
--------------------------------------------------------------------------------
/.ddev/providers/upsun.yaml:
--------------------------------------------------------------------------------
1 | #ddev-generated
2 | # Upsun provider configuration. This works out of the box, but can be edited to add
3 | # your own preferences. If you edit it, remove the `ddev-generated` line from the top so
4 | # that it won't be overwritten.
5 |
6 | # To use this configuration,
7 |
8 | # 1. Check out the site from Upsun and then configure it with `ddev config`. You'll want to use `ddev start` and make sure the basic functionality is working.
9 | # 2. Obtain and configure an API token.
10 | # a. Login to the Upsun Dashboard and go to My Profile->API Tokens to create an API token for DDEV to use.
11 | # b. Add the API token to the `web_environment` section in your global ddev configuration at ~/.ddev/global_config.yaml:
12 | # ```yaml
13 | # web_environment:
14 | # - UPSUN_CLI_TOKEN=abcdeyourtoken
15 | # ```
16 | # 3. Add UPSUN_PROJECT and UPSUN_ENVIRONMENT variables to your project `.ddev/config.yaml` or a `.ddev/config.upsun.yaml`
17 | # ```yaml
18 | # web_environment:
19 | # - UPSUN_PROJECT=nf4amudfn23biyourproject
20 | # - UPSUN_ENVIRONMENT=main
21 | # 4. `ddev restart`
22 | # 5. Run `ddev pull upsun`. After you agree to the prompt, the current upstream database and files will be downloaded.
23 | # 6. Optionally use `ddev push upsun` to push local files and database to Upsun. Note that `ddev push` is a command that can potentially damage your production site, so this is not recommended.
24 |
25 | # Debugging: Use `ddev exec upsun` to see what Upsun knows about
26 | # your configuration and whether it's working correctly.
27 |
28 | auth_command:
29 | command: |
30 | set -eu -o pipefail
31 | if [ -z "${UPSUN_CLI_TOKEN:-}" ]; then echo "Please make sure you have set UPSUN_CLI_TOKEN." && exit 1; fi
32 | if [ -z "${UPSUN_PROJECT:-}" ]; then echo "Please make sure you have set UPSUN_PROJECT." && exit 1; fi
33 | if [ -z "${UPSUN_ENVIRONMENT:-}" ]; then echo "Please make sure you have set UPSUN_ENVIRONMENT." && exit 1; fi
34 |
35 | db_pull_command:
36 | command: |
37 | # set -x # You can enable bash debugging output by uncommenting
38 | set -eu -o pipefail
39 | export UPSUN_CLI_NO_INTERACTION=1
40 | ls /var/www/html/.ddev >/dev/null # This just refreshes stale NFS if possible
41 | upsun db:dump --yes --gzip --file=/var/www/html/.ddev/.downloads/db.sql.gz --project="${UPSUN_PROJECT}" --environment="${UPSUN_ENVIRONMENT}"
42 |
43 | files_import_command:
44 | command: |
45 | # set -x # You can enable bash debugging output by uncommenting
46 | set -eu -o pipefail
47 | export UPSUN_CLI_NO_INTERACTION=1
48 | # Use $UPSUN_MOUNTS if it exists to get list of mounts to download, otherwise just web/sites/default/files (drupal)
49 | declare -a mounts=(${UPSUN_MOUNTS:-/web/sites/default/files})
50 | upsun mount:download --all --yes --quiet --project="${UPSUN_PROJECT}" --environment="${UPSUN_ENVIRONMENT}" --target=/var/www/html
51 |
52 |
53 | # push is a dangerous command. If not absolutely needed it's better to delete these lines.
54 | db_push_command:
55 | command: |
56 | # set -x # You can enable bash debugging output by uncommenting
57 | set -eu -o pipefail
58 | export UPSUN_CLI_NO_INTERACTION=1
59 | ls /var/www/html/.ddev >/dev/null # This just refreshes stale NFS if possible
60 | pushd /var/www/html/.ddev/.downloads >/dev/null
61 | gzip -dc db.sql.gz | upsun db:sql --project="${UPSUN_PROJECT}" --environment="${UPSUN_ENVIRONMENT}"
62 |
63 | # push is a dangerous command. If not absolutely needed it's better to delete these lines.
64 | # TODO: This is a naive, Drupal-centric push, which needs adjustment for the mount to be pushed.
65 | files_push_command:
66 | command: |
67 | # set -x # You can enable bash debugging output by uncommenting
68 | set -eu -o pipefail
69 | export UPSUN_CLI_NO_INTERACTION=1
70 | ls "${DDEV_FILES_DIR}" >/dev/null # This just refreshes stale NFS if possible
71 | upsun mount:upload --yes --quiet --project="${UPSUN_PROJECT}" --environment="${UPSUN_ENVIRONMENT}" --source="${DDEV_FILES_DIR}" --mount=web/sites/default/files
72 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG-REPORT.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Let us know about a problem with Odds & Ends
3 | labels:
4 | - "bug report"
5 | - "bug report status: new"
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | Thanks for taking the time to fill out this bug report! Please check to make sure your bug hasn't already been reported before proceeding. Please also make sure you correctly follow all instructions when filling out this form. Failure to do so may result in your bug report being closed as invalid.
11 | - type: textarea
12 | id: bug-description
13 | attributes:
14 | label: Bug Description
15 | description: Details of the bug you've encountered.
16 | validations:
17 | required: true
18 | - type: textarea
19 | id: steps-to-reproduce
20 | attributes:
21 | label: Steps to reproduce
22 | description: Instructions that can be followed to reliably reproduce the bug.
23 | validations:
24 | required: true
25 | - type: textarea
26 | id: expected-behaviour
27 | attributes:
28 | label: Expected behaviour
29 | description: What should be expected to happen instead of the buggy behaviour.
30 | - type: input
31 | id: plugin-version
32 | attributes:
33 | label: Odds & Ends version
34 | description: Enter the exact Odds & Ends version number you are using.
35 | validations:
36 | required: true
37 | - type: input
38 | id: craft-cms-version
39 | attributes:
40 | label: Craft CMS version
41 | description: Enter the exact Craft CMS version number you are using.
42 | validations:
43 | required: true
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest an idea for Odds & Ends
3 | labels:
4 | - enhancement
5 | body:
6 | - type: textarea
7 | id: feature-description
8 | attributes:
9 | label: What would you like to see added to or changed in Odds & Ends, and why?
10 | validations:
11 | required: true
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/QUESTION.yml:
--------------------------------------------------------------------------------
1 | name: Question
2 | description: Ask a question about Odds & Ends
3 | labels:
4 | - question
5 | body:
6 | - type: textarea
7 | id: question
8 | attributes:
9 | label: What question would you like to ask?
10 | validations:
11 | required: true
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # CRAFT ENVIRONMENT
2 | .env.php
3 | .env.sh
4 | .env
5 |
6 | # COMPOSER
7 | /vendor
8 |
9 | # BUILD FILES
10 | /bower_components/*
11 | /node_modules/*
12 | /build/*
13 | /yarn-error.log
14 |
15 | # MISC FILES
16 | .cache
17 | .DS_Store
18 | .idea
19 | .project
20 | .settings
21 | *.esproj
22 | *.sublime-workspace
23 | *.sublime-project
24 | *.tmproj
25 | *.tmproject
26 | .vscode/*
27 | !.vscode/settings.json
28 | !.vscode/tasks.json
29 | !.vscode/launch.json
30 | !.vscode/extensions.json
31 | config.codekit3
32 | prepros-6.config
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
6 |
7 | ## 5.0.0 - 2024-05-01
8 |
9 | ### Added
10 | - Added Craft 5 compatibility
11 |
12 | ### Removed
13 | - Removed Craft 4 compatibility
14 |
15 | ## 4.3.1 - 2023-09-14
16 |
17 | ### Fixed
18 | - Fixed a bug where fields from version 2.x wouldn't be migrated on projects that aren't using the project config (thanks @jamie-s-white)
19 |
20 | ## 4.3.0 - 2023-07-02
21 |
22 | ### Added
23 | - Added `spicyweb\oddsandends\fields\DisabledUsers` - Users (Disabled) field type
24 |
25 | ## 4.2.0 - 2023-03-27
26 |
27 | ### Added
28 | - Added the ability to disable plugin components via the new `disableNormalFields`, `disableCommerceFields`, and `disableWidgets` plugin settings (thanks @kbergha)
29 |
30 | ### Changed
31 | - Odds & Ends now requires Craft 4.4.0 or later
32 |
33 | ### Fixed
34 | - Fixed an error that occurred on Craft 4.4.0 or later
35 |
36 | ## 4.1.2 - 2023-03-09
37 |
38 | ### Fixed
39 | - Fixed an error that occurred if the Roll Your Own widget was set to use a nonexistent template
40 |
41 | ## 4.1.1 - 2022-11-30
42 |
43 | ### Fixed
44 | - Fixed an error that occurred when executing a GraphQL query if a Width field existed in the Craft install
45 |
46 | ## 4.1.0 - 2022-10-20
47 |
48 | ### Added
49 | - Added `spicyweb\oddsandends\fields\DisabledProducts` - Commerce Products (Disabled) field type
50 | - Added `spicyweb\oddsandends\fields\DisabledVariants` - Commerce Variants (Disabled) field type
51 | - Added `spicyweb\oddsandends\fields\ProductsSearch` - Commerce Products (Search) field type
52 | - Added `spicyweb\oddsandends\fields\VariantsSearch` - Commerce Variants (Search) field type
53 |
54 | ### Fixed
55 | - Fixed some style issues with element search fields
56 |
57 | ## 4.0.0 - 2022-10-19
58 |
59 | ### Added
60 | - Added support for Craft 4 (requires Craft 4.2.1 or later)
61 |
62 | ### Removed
63 | - Removed support for Craft 3
64 |
65 | ## 3.0.3 - 2023-09-14
66 |
67 | ### Fixed
68 | - Fixed a bug where fields from version 2.x wouldn't be migrated on projects that aren't using the project config (thanks @jamie-s-white)
69 |
70 | ## 3.0.2 - 2022-10-20
71 |
72 | ### Fixed
73 | - Fixed an error that could occur when editing element search field contents
74 | - Fixed a bug where previously selected elements for element search fields couldn't be removed
75 |
76 | ## 3.0.1 - 2022-10-19
77 |
78 | ### Fixed
79 | - Fixed an error that occurred when attempting inline editing of entries selected for an Entries (Search) field, and categories selected for a Categories (Search) field
80 |
81 | ## 3.0.0 - 2022-10-18
82 |
83 | > {note} The plugin’s package name has changed to `spicyweb/craft-odds-and-ends`. If you’re updating with Composer, you will need to run `composer require spicyweb/craft-odds-and-ends` and then `composer remove supercool/tools`.
84 |
85 | ### Changed
86 | - Name changed from 'Tools' to 'Odds & Ends'
87 | - Now maintained by Spicy Web
88 | - Now requires Craft 3.7.55.3 or later Craft 3 releases
89 |
90 | ### Fixed
91 | - Fixed an error that occurred when creating a width field on Craft 3.7.46 or later Craft 3 releases
92 | - Fixed a bug where Roll Your Own widgets were always displaying 'Roll Your Own' as the title, instead of the user-specified title
93 | - Fixed a JavaScript error that could occur with Entries (Search) and Categories (Search) fields
94 | - Fixed a bug where Entries (Search) and Categories (Search) fields could fail to return results
95 | - Fixed an error that occurred with the Entries (Search) field if single sections were one of the sources set for the field
96 | - Fixed an error that occurred when loading Ancestors field settings
97 | - Fixed a bug where Ancestors field modals could fail to show elements
98 |
99 | ## 2.2.3 - 2022-05-24
100 |
101 | ### Fixed
102 | - Width field styling
103 |
104 | ## 2.2.2 - 2022-05-09
105 |
106 | ### Fixed
107 | - Width field data type for php 7.4
108 |
109 | ## 2.2.1.1 - 2021-01-20
110 |
111 | ### Fixed
112 | - Validation error when grid data was typecasted to int
113 |
114 | ## 2.2.1 - 2021-01-19
115 |
116 | ### Changed
117 | - Settings and UI for grid field
118 |
119 | ## 2.2.0 - 2020-09-24
120 |
121 | ### Added
122 | - Support for craft 3.5
123 |
124 | ## 2.1.10.1 - 2021-01-20
125 |
126 | ### Fixed
127 | - Small validation fix for grid field rework
128 |
129 | ## 2.1.10 - 2021-01-19
130 |
131 | ### Changed
132 | - Grid field settings and UI overhauled
133 |
134 | ## 2.1.9 - 2020-10-07
135 |
136 | ### Fixed
137 | - Fixed width field for Craft 3.3.5+
138 |
139 | ## 2.1.8 - 2019-03-15
140 |
141 | ### Added
142 | - Added `supercool\tools\fields\data\GridData::leftRight()` field for use in templates
143 |
144 | ## 2.1.7 - 2019-03-04
145 |
146 | ### Fixed
147 | - Fixed a composer.json issue
148 |
149 | ## 2.1.6 - 2019-03-04
150 |
151 | ### Fixed
152 | - Fixed an error that could occur with width fields
153 |
154 | ## 2.1.5 - 2018-12-17
155 |
156 | ### Added
157 | - Added default value for width
158 |
159 | ## 2.1.4 - 2018-12-13
160 |
161 | ### Added
162 | - Added global settings for width
163 |
164 | ## 2.1.3 - 2018-12-11
165 |
166 | ### Fixed
167 | - Fixed multiple width fields
168 |
169 | ## 2.1.2 - 2018-12-06
170 |
171 | ### Added
172 | - Added toggle for show/hide width field
173 |
174 | ## 2.1.1 - 2018-11-13
175 |
176 | ### Fixed
177 | - Fixed a composer.json issue
178 |
179 | ## 2.1.0 - 2018-11-13
180 |
181 | ### Added
182 | - Added a grid field type which is going to be used as a width field
183 |
184 | ## 2.0.0 - 2018-08-02
185 |
186 | ### Added
187 | - Initial Craft CMS 3 release
188 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022-2024 Spicy Web
4 | Copyright (c) 2018 Supercool Ltd
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Odds & Ends
4 |
5 | A collection of useful tools for Craft CMS websites.
6 |
7 | ## Field types
8 |
9 | ### Width
10 | This lets you define the width of a block as well as left and right padding. This field simply outputs three sets of classes which can be defined when setting the field up.
11 |
12 | 
13 | 
14 |
15 | ### Author Instructions
16 | This lets you output markdown instead of a field, which is useful when you have a Matrix block that doesn’t have any fields.
17 |
18 | 
19 |
20 | ### Categories (multiple groups)
21 | A Categories input that lets you select multiple Category groups.
22 |
23 | ### Ancestors
24 | An Entries input that only shows the ancestors of the current Entry.
25 |
26 | ### Search Fields
27 | Like the Tags input, but for entries and categories (without the auto-creation feature). If Craft Commerce is installed, search fields for products and variants are also available.
28 |
29 | ### Disabled Fields
30 | The same as regular fields, but disabled. Useful for situations where you want to integrate with a third party API and store that information in a field but don’t want the user to change it.
31 |
32 | The following field types are currently supported:
33 |
34 | - Entries
35 | - Categories
36 | - Lightswitch
37 | - Number
38 | - PlainText
39 | - Dropdown
40 | - Commerce Products
41 | - Commerce Variants
42 |
43 | ## Widgets
44 |
45 | ### Roll Your Own
46 | A simple widget that lets you assign a template to load from your site templates folder. Go nuts.
47 |
48 | ## Miscellaneous
49 |
50 | ### Download File
51 |
52 | A controller action that will download an asset file.
53 |
54 | The `id` parameter is required and must be a valid asset ID.
55 |
56 | Usage:
57 | ```
58 | Download
59 | ```
60 |
61 |
62 | ## Configuration
63 |
64 | By default, all normal field types and widgets are enabled.
65 | The commerce field types are only enabled if Craft Commerce is installed and enabled.
66 | You can disable each field type and widget by adding the following to your project's `config/tools.php` file:
67 |
68 | ```php
69 | use spicyweb\oddsandends\fields\AuthorInstructions;
70 | use spicyweb\oddsandends\fields\DisabledProducts;
71 | use spicyweb\oddsandends\widgets\RollYourOwn;
72 |
73 | return [
74 | 'disableNormalFields' => [
75 | AuthorInstructions::class,
76 | ],
77 | 'disableCommerceFields' => [
78 | DisabledProducts::class,
79 | ],
80 | 'disableWidgets' => [
81 | RollYourOwn::class,
82 | ],
83 | ];
84 | ```
85 |
86 | Multi environment config is supported. See [Craft's docs](https://craftcms.com/docs/4.x/config/#multi-environment-configs) for more info.
87 | See `config/tools.php` in this repo for an example on how to disable any field and widget.
88 |
89 | ---
90 |
91 | *Created by [Supercool](https://supercooldesign.co.uk)*
92 |
93 | *Maintained by [Spicy Web](https://spicyweb.com.au)*
94 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spicyweb/craft-odds-and-ends",
3 | "description": "A collection of useful tools for Craft CMS websites",
4 | "type": "craft-plugin",
5 | "version": "5.0.0",
6 | "keywords": [
7 | "craft",
8 | "cms",
9 | "craftcms",
10 | "craft-plugin"
11 | ],
12 | "support": {
13 | "docs": "https://github.com/spicywebau/craft-odds-and-ends/blob/5.0.0/README.md",
14 | "issues": "https://github.com/spicywebau/craft-odds-and-ends/issues"
15 | },
16 | "license": "MIT",
17 | "authors": [
18 | {
19 | "name": "Spicy Web",
20 | "homepage": "https://spicyweb.com.au"
21 | }
22 | ],
23 | "require": {
24 | "craftcms/cms": "^5.0.0",
25 | "php": "^8.2"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "spicyweb\\oddsandends\\": "src/"
30 | }
31 | },
32 | "extra": {
33 | "name": "Odds & Ends",
34 | "handle": "tools",
35 | "schemaVersion": "4.3.1",
36 | "hasCpSettings": false,
37 | "hasCpSection": false,
38 | "changelogUrl": "https://github.com/spicywebau/craft-odds-and-ends/blob/5.x/CHANGELOG.md",
39 | "class": "spicyweb\\oddsandends\\Tools"
40 | },
41 | "minimum-stability": "dev",
42 | "prefer-stable": true,
43 | "require-dev": {
44 | "craftcms/ecs": "dev-main",
45 | "craftcms/rector": "dev-main"
46 | },
47 | "config": {
48 | "allow-plugins": {
49 | "yiisoft/yii2-composer": true,
50 | "craftcms/plugin-installer": true
51 | }
52 | },
53 | "scripts": {
54 | "check-cs": "ecs check --ansi",
55 | "fix-cs": "ecs check --ansi --fix"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/config/tools.php:
--------------------------------------------------------------------------------
1 | [
34 | // Ancestors::class,
35 | // AuthorInstructions::class,
36 | // CategoriesMultipleGroups::class,
37 | // CategoriesSearch::class,
38 | // DisabledCategories::class,
39 | // DisabledDropdown::class,
40 | // DisabledEntries::class,
41 | // DisabledLightswitch::class,
42 | // DisabledNumber::class,
43 | // DisabledPlainText::class,
44 | // DisabledUsers::class,
45 | // EntriesSearch::class,
46 | // Grid::class,
47 | // Width::class,
48 | ],
49 | 'disableCommerceFields' => [
50 | // DisabledProducts::class,
51 | // DisabledVariants::class,
52 | // ProductsSearch::class,
53 | // VariantsSearch::class,
54 | ],
55 | 'disableWidgets' => [
56 | // RollYourOwn::class,
57 | ],
58 | ];
59 |
--------------------------------------------------------------------------------
/docs/assets/author-instructions-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spicywebau/craft-odds-and-ends/525607a50912cb9d73aa4f018ef7fa9423dc83aa/docs/assets/author-instructions-example.png
--------------------------------------------------------------------------------
/docs/assets/width-field.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spicywebau/craft-odds-and-ends/525607a50912cb9d73aa4f018ef7fa9423dc83aa/docs/assets/width-field.png
--------------------------------------------------------------------------------
/docs/assets/width-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spicywebau/craft-odds-and-ends/525607a50912cb9d73aa4f018ef7fa9423dc83aa/docs/assets/width-settings.png
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | parallel();
10 | $ecsConfig->paths([
11 | __DIR__ . '/src',
12 | __FILE__,
13 | ]);
14 |
15 | $ecsConfig->sets([SetList::CRAFT_CMS_3]);
16 | };
17 |
--------------------------------------------------------------------------------
/src/Tools.php:
--------------------------------------------------------------------------------
1 |
40 | * @author Supercool
41 | * @since 2.0.0
42 | */
43 | class Tools extends Plugin
44 | {
45 | /**
46 | * @var Tools The plugin instance.
47 | */
48 | public static ?Tools $plugin = null;
49 |
50 | /**
51 | * @inheritdoc
52 | */
53 | public string $minVersionRequired = '4.3.1';
54 |
55 | /**
56 | * @inheritdoc
57 | */
58 | public string $schemaVersion = '4.3.1';
59 |
60 | /**
61 | * @inheritdoc
62 | */
63 | public function init(): void
64 | {
65 | parent::init();
66 | self::$plugin = $this;
67 |
68 | // Register our fields
69 | Event::on(
70 | Fields::className(),
71 | Fields::EVENT_REGISTER_FIELD_TYPES,
72 | function(RegisterComponentTypesEvent $event) {
73 | $enableNormalFields = [
74 | AuthorInstructionsField::class,
75 | DisabledLightswitchField::class,
76 | DisabledPlainTextField::class,
77 | DisabledNumberField::class,
78 | DisabledEntriesField::class,
79 | DisabledCategoriesField::class,
80 | DisabledDropdownField::class,
81 | DisabledUsersField::class,
82 | EntriesSearchField::class,
83 | CategoriesSearchField::class,
84 | CategoriesMultipleGroupsField::class,
85 | WidthField::class,
86 | AncestorsField::class,
87 | GridField::class,
88 | ];
89 |
90 | $enableNormalFields = array_diff($enableNormalFields, $this->settings->disableNormalFields);
91 | Craft::debug($this->name.' enable normal fields: ' . implode(', ', $enableNormalFields), __METHOD__);
92 |
93 | foreach ($enableNormalFields as $field) {
94 | $event->types[] = $field;
95 | }
96 |
97 | $pluginsService = Craft::$app->getPlugins();
98 | if ($pluginsService->isPluginInstalled('commerce') && $pluginsService->isPluginEnabled('commerce')) {
99 | $enableCommerceFields = [
100 | DisabledProductsField::class,
101 | DisabledVariantsField::class,
102 | ProductsSearchField::class,
103 | VariantsSearchField::class,
104 | ];
105 |
106 | $enableCommerceFields = array_diff($enableCommerceFields, $this->settings->disableCommerceFields);
107 | Craft::debug($this->name.' enable commerce fields: ' . implode(', ', $enableCommerceFields), __METHOD__);
108 |
109 | foreach ($enableCommerceFields as $field) {
110 | $event->types[] = $field;
111 | }
112 | }
113 | }
114 | );
115 |
116 | // Register widgets
117 | Event::on(
118 | Dashboard::className(),
119 | Dashboard::EVENT_REGISTER_WIDGET_TYPES,
120 | function(RegisterComponentTypesEvent $event) {
121 | $enableWidgets = [
122 | RollYourOwnWidget::class,
123 | ];
124 |
125 | $enableWidgets = array_diff($enableWidgets, $this->settings->disableWidgets);
126 | Craft::debug($this->name.' enable widgets: ' . implode(', ', $enableWidgets), __METHOD__);
127 |
128 | foreach ($enableWidgets as $widget) {
129 | $event->types[] = $widget;
130 | }
131 | }
132 | );
133 |
134 | Craft::info(Craft::t('tools', '{name} plugin loaded', ['name' => $this->name]), __METHOD__);
135 | }
136 |
137 | /**
138 | * @inheritdoc
139 | */
140 | protected function createSettingsModel(): ?Model
141 | {
142 | return new Settings();
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/assetbundles/tools/ToolsAsset.php:
--------------------------------------------------------------------------------
1 |
13 | * @author Supercool
14 | * @since 2.0.0
15 | */
16 | class ToolsAsset extends AssetBundle
17 | {
18 | /**
19 | * @inheritdoc
20 | */
21 | public function init(): void
22 | {
23 | // define the path that your publishable resources live
24 | $this->sourcePath = "@spicyweb/oddsandends/assetbundles/tools/dist";
25 |
26 | // define the dependencies
27 | $this->depends = [
28 | CpAsset::class,
29 | ];
30 |
31 | // define the relative path to CSS/JS files that should be registered with the page
32 | // when this asset bundle is registered
33 | $this->js = [
34 | 'js/nouislider.js',
35 | 'js/tools.js',
36 | ];
37 |
38 | $this->css = [
39 | 'css/nouislider.css',
40 | 'css/nouislider.pips.css',
41 | 'css/tools.css',
42 | ];
43 |
44 | parent::init();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/assetbundles/tools/dist/css/nouislider.css:
--------------------------------------------------------------------------------
1 | /* Functional styling;
2 | * These styles are required for noUiSlider to function.
3 | * You don't need to change these rules to apply your design.
4 | */
5 | .noUi-target,
6 | .noUi-target * {
7 | -webkit-touch-callout: none;
8 | -webkit-user-select: none;
9 | -ms-touch-action: none;
10 | touch-action: none;
11 | -ms-user-select: none;
12 | -moz-user-select: none;
13 | user-select: none;
14 | -moz-box-sizing: border-box;
15 | box-sizing: border-box;
16 | }
17 | .noUi-target {
18 | position: relative;
19 | direction: ltr;
20 | }
21 | .noUi-base {
22 | width: 100%;
23 | height: 100%;
24 | position: relative;
25 | z-index: 1; /* Fix 401 */
26 | }
27 | .noUi-connect {
28 | position: absolute;
29 | right: 0;
30 | top: 0;
31 | left: 0;
32 | bottom: 0;
33 | }
34 | .noUi-origin {
35 | position: absolute;
36 | height: 0;
37 | width: 0;
38 | }
39 | .noUi-handle {
40 | position: relative;
41 | z-index: 1;
42 | cursor: col-resize !important;
43 | }
44 |
45 | .noUi-state-tap .noUi-connect,
46 | .noUi-state-tap .noUi-origin {
47 | -webkit-transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
48 | transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
49 | }
50 | .noUi-state-drag * {
51 | cursor: grabbing;
52 | }
53 |
54 | /* Painting and performance;
55 | * Browsers can paint handles in their own layer.
56 | */
57 | .noUi-base,
58 | .noUi-handle {
59 | -webkit-transform: translate3d(0,0,0);
60 | transform: translate3d(0,0,0);
61 | }
62 |
63 | /* Slider size and handle placement;
64 | */
65 | .noUi-horizontal {
66 | height: 18px;
67 | }
68 | .noUi-horizontal .noUi-handle {
69 | width: 20px;
70 | height: 18px;
71 | left: 0;
72 | }
73 |
74 | .noUi-horizontal .noUi-handle:hover {
75 | animation: pulse 1s infinite;
76 | }
77 |
78 | @keyframes pulse {
79 | 0% {
80 | transform: scale(1)
81 | }
82 | 50% {
83 | transform: scale(1.2)
84 | }
85 | 100% {
86 | transform: scale(1)
87 | }
88 | }
89 |
90 | .noUi-horizontal .noUi-handle.grid__secondHandle {
91 | left: -20px;
92 | }
93 |
94 | /* Styling;
95 | */
96 | .noUi-target {
97 | background: #FAFAFA;
98 | border-radius: 4px;
99 | border: 1px solid #D3D3D3;
100 | box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
101 | }
102 |
103 | /* Handles and cursors;
104 | */
105 | .noUi-draggable {
106 | cursor: grab;
107 | display: flex;
108 | justify-content: center;
109 | align-items: center;
110 | font-weight: bold;
111 | font-size: 12px;
112 | color: #fafafa;
113 | background: #566576;
114 | border-radius: 3px;
115 | transition: background .3s;
116 | }
117 |
118 | .is-at-limit .noUi-draggable {
119 | background: #da5a47;
120 | }
121 |
122 | .noUi-state-drag .noUi-draggable {
123 | cursor: grabbing;
124 | }
125 |
126 | .noUi-vertical .noUi-draggable {
127 | cursor: n-resize;
128 | }
129 | .noUi-handle {
130 | border: 1px solid #D9D9D9;
131 | border-radius: 3px;
132 | background: #FFF;
133 | cursor: default;
134 | box-shadow: inset 0 0 1px #FFF,
135 | inset 0 1px 7px #EBEBEB,
136 | 0 3px 6px -3px #BBB;
137 | }
138 | .noUi-active {
139 | box-shadow: inset 0 0 1px #FFF,
140 | inset 0 1px 7px #DDD,
141 | 0 3px 6px -3px #BBB;
142 | }
143 |
144 | /* Handle stripes;
145 | */
146 | .noUi-handle:before,
147 | .noUi-handle:after {
148 | content: "";
149 | display: block;
150 | background: #E8E7E6;
151 | }
152 | .noUi-handle:after {
153 | left: 17px;
154 | }
155 |
156 | /* Disabled state;
157 | */
158 |
159 | [disabled] .noUi-connect {
160 | background: #B8B8B8;
161 | }
162 | [disabled].noUi-target,
163 | [disabled].noUi-handle,
164 | [disabled] .noUi-handle {
165 | cursor: not-allowed;
166 | }
167 |
168 | /*----------------------------------------------------
169 | Custom Style
170 | -----------------------------------------------------*/
171 |
172 | .noUi-target {
173 | box-shadow: none;
174 | border: 1px solid rgba(0, 0, 20, 0.1);
175 | height: 20px;
176 | margin-top: 10px;
177 | margin-bottom: 0px;
178 | background: #ffffff;
179 | }
180 |
181 | .noUi-pips .noUi-value-sub, .noUi-pips .noUi-value-large, .noUi-pips .noUi-marker-large {
182 | display: none;
183 | }
184 |
185 | .noUi-pips .noUi-marker-sub, .noUi-pips .noUi-marker-large {
186 | top: -0;
187 | height: 14px;
188 | width: 4px;
189 | background: rgba(0, 0, 20, 0.1);
190 | }
191 |
192 | .noUi-handle {
193 | border: none;
194 | border-radius: 0px;
195 | box-shadow: none;
196 | background: none;
197 | display: flex;
198 | justify-content: center;
199 | align-items: center;
200 | }
201 |
202 | .noUi-handle:before {
203 | display: none;
204 | }
205 |
206 | .noUi-handle:after {
207 | content: "move";
208 | background: none;
209 | font-family: 'Craft';
210 | word-wrap: normal !important;
211 | color: rgba(0,0,0,0.2);
212 | font-size: 12px;
213 | }
214 |
215 | .noUi__output {
216 | background-color: #BDC0C4;
217 | display: inline-block;
218 | margin-top: 10px;
219 | padding: 3px;
220 | border-radius: 2px;
221 | font-size: 12px;
222 | color: #fff;
223 | }
224 |
225 | .noUi__total {
226 | background-color: #BDC0C4;
227 | display: inline-block;
228 | padding: 3px;
229 | border-radius: 2px;
230 | position: absolute;
231 | top: 0;
232 | right: 0;
233 | font-size: 10px;
234 | color: #fff;
235 | }
236 |
237 | .noUi__info-block {
238 | display: inline-block;
239 | width: 50%;
240 | color: #BDC0C4;
241 | font-size: 10px;
242 | }
243 |
244 | .noUi__info-block div {
245 | display: inline-block;
246 | margin-right: 5px;
247 | }
248 |
249 | .noUi__width-info span {
250 | display: inline-block;
251 | width: 8px;
252 | height: 8px;
253 | background-color: #DC5942;
254 | vertical-align: inherit;
255 | }
256 |
257 | .noUi__margin-info span {
258 | display: inline-block;
259 | width: 8px;
260 | height: 8px;
261 | background-color: #566576;
262 | vertical-align: inherit;
263 | }
264 |
265 | .noUi__grid-value {
266 | display: block;
267 | width: 75px;
268 | background-color: #BDC0C4;
269 | padding: 2px;
270 | border-radius: 2px;
271 | font-size: 12px;
272 | color: #fff;
273 | text-align: center;
274 | }
275 |
276 | /* hacky targeting fix for craft 2 width field */
277 | [data-type*="Width"] .noUi-horizontal .noUi-handle {
278 | width: 15px;
279 | height: 15px;
280 | left: -16px;
281 | top: -6px;
282 | }
283 |
284 | [data-type*="Width"] .noUi-horizontal {
285 | height: 18px;
286 | }
287 | [data-type*="Width"] .noUi-horizontal .noUi-handle {
288 | width: 15px;
289 | height: 15px;
290 | left: -16px;
291 | top: -6px;
292 | }
293 | [data-type*="Width"] .noUi-vertical {
294 | width: 18px;
295 | }
296 | [data-type*="Width"] .noUi-vertical .noUi-handle {
297 | width: 28px;
298 | height: 34px;
299 | left: -6px;
300 | top: -17px;
301 | }
302 |
303 | [data-type*="Width"] .noUi-target {
304 | background: #FAFAFA;
305 | border-radius: 4px;
306 | border: 1px solid #D3D3D3;
307 | box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
308 | }
309 | [data-type*="Width"] .noUi-connect {
310 | background: #3FB8AF;
311 | box-shadow: inset 0 0 3px rgba(51,51,51,0.45);
312 | -webkit-transition: background 450ms;
313 | transition: background 450ms;
314 | }
315 |
316 | [data-type*="Width"] .noUi-draggable {
317 | cursor: w-resize;
318 | }
319 | [data-type*="Width"] .noUi-vertical .noUi-draggable {
320 | cursor: n-resize;
321 | }
322 | [data-type*="Width"] .noUi-handle {
323 | border: 1px solid #D9D9D9;
324 | border-radius: 3px;
325 | background: #FFF;
326 | cursor: default;
327 | box-shadow: inset 0 0 1px #FFF,
328 | inset 0 1px 7px #EBEBEB,
329 | 0 3px 6px -3px #BBB;
330 | }
331 | [data-type*="Width"] .noUi-active {
332 | box-shadow: inset 0 0 1px #FFF,
333 | inset 0 1px 7px #DDD,
334 | 0 3px 6px -3px #BBB;
335 | }
336 |
337 | [data-type*="Width"] .noUi-handle:before,
338 | [data-type*="Width"] .noUi-handle:after {
339 | content: "";
340 | display: block;
341 | position: absolute;
342 | height: 14px;
343 | width: 1px;
344 | background: #E8E7E6;
345 | left: 14px;
346 | top: 6px;
347 | }
348 | [data-type*="Width"] .noUi-handle:after {
349 | left: 17px;
350 | }
351 | [data-type*="Width"] .noUi-vertical .noUi-handle:before,
352 | [data-type*="Width"] .noUi-vertical .noUi-handle:after {
353 | width: 14px;
354 | height: 1px;
355 | left: 6px;
356 | top: 14px;
357 | }
358 | [data-type*="Width"] .noUi-vertical .noUi-handle:after {
359 | top: 17px;
360 | }
361 |
362 | [data-type*="Width"] .noUi-target {
363 | border-radius: 0px;
364 | box-shadow: none;
365 | border: none;
366 | height: 5px;
367 | background: #BCBFC3;
368 | margin-top: 50px;
369 | margin-bottom: 50px;
370 | }
371 |
372 | [data-type*="Width"] .noUi-pips .noUi-value-sub, [data-type*="Width"] .noUi-pips .noUi-value-large {
373 | display: none;
374 | }
375 |
376 | [data-type*="Width"] .noUi-pips .noUi-marker-sub, [data-type*="Width"] .noUi-pips .noUi-marker-large {
377 | display: block;
378 | top: -5px;
379 | height: 14px;
380 | width: 2px;
381 | background: #BCBFC3;
382 | }
383 |
384 | [data-type*="Width"] .noUi-base .noUi-connect:nth-child(4) {
385 | background: #DC5942;
386 | }
387 |
388 | [data-type*="Width"] .noUi-base .noUi-connect:nth-child(6), [data-type*="Width"] .noUi-base .noUi-connect:nth-child(2) {
389 | background: #566576;
390 | }
391 |
392 | [data-type*="Width"] .firstHandle, .fourthHandle {
393 | top: 10px !important;
394 | }
395 |
396 | [data-type*="Width"] .secondHandle, .thirdHandle {
397 | top: -30px !important;
398 | }
399 |
400 | [data-type*="Width"] .noUi-handle {
401 | border: none;
402 | border-radius: 0px;
403 | box-shadow: none;
404 | background: none;
405 | }
406 |
407 | [data-type*="Width"] .noUi-handle:before {
408 | display: none;
409 | }
410 |
411 | [data-type*="Width"] .noUi-handle:after {
412 | content: "move";
413 | position: absolute;
414 | top: 0;
415 | left: 11px;
416 | background: none;
417 | font-family: 'Craft';
418 | word-wrap: normal !important;
419 | color: rgba(0,0,0,0.2);
420 | cursor: move;
421 | }
422 |
423 | [data-type*="Width"] .noUi-handle:hover:after {
424 | color: #0d78f2;
425 | }
426 |
427 | [data-type*="Width"] .noUi__output {
428 | background-color: #BDC0C4;
429 | display: inline-block;
430 | margin-top: 10px;
431 | padding: 3px;
432 | border-radius: 2px;
433 | font-size: 12px;
434 | color: #fff;
435 | }
436 |
437 | [data-type*="Width"] .noUi__total {
438 | background-color: #BDC0C4;
439 | display: inline-block;
440 | padding: 3px;
441 | border-radius: 2px;
442 | position: absolute;
443 | top: 32px;
444 | right: 0;
445 | font-size: 10px;
446 | color: #fff;
447 | }
448 |
449 | [data-type*="Width"] .noUi__info-block {
450 | position: absolute;
451 | top: 36px;
452 | left: 0;
453 | color: #BDC0C4;
454 | font-size: 10px;
455 | }
456 |
457 | [data-type*="Width"] .noUi__info-block div {
458 | display: inline-block;
459 | margin-right: 5px;
460 | }
461 |
462 | [data-type*="Width"] .noUi__width-info span {
463 | display: inline-block;
464 | width: 8px;
465 | height: 8px;
466 | background-color: #DC5942;
467 | vertical-align: inherit;
468 | }
469 |
470 | [data-type*="Width"] .noUi__margin-info span {
471 | display: inline-block;
472 | width: 8px;
473 | height: 8px;
474 | background-color: #566576;
475 | vertical-align: inherit;
476 | }
--------------------------------------------------------------------------------
/src/assetbundles/tools/dist/css/nouislider.pips.css:
--------------------------------------------------------------------------------
1 |
2 | /* Base;
3 | *
4 | */
5 | .noUi-pips,
6 | .noUi-pips * {
7 | -moz-box-sizing: border-box;
8 | box-sizing: border-box;
9 | }
10 | .noUi-pips {
11 | position: absolute;
12 | color: #999;
13 | z-index: 1;
14 | pointer-events: none;
15 | }
16 |
17 | /* Values;
18 | *
19 | */
20 | .noUi-value {
21 | position: absolute;
22 | text-align: center;
23 | }
24 | .noUi-value-sub {
25 | color: #ccc;
26 | font-size: 10px;
27 | }
28 |
29 | /* Markings;
30 | *
31 | */
32 | .noUi-marker {
33 | position: absolute;
34 | background: rgba(0, 0, 20, 0.1);
35 | }
36 | .noUi-marker-sub {
37 | background: rgba(0, 0, 20, 0.1);
38 | }
39 | .noUi-marker-large {
40 | background: rgba(0, 0, 20, 0.1);
41 | }
42 |
43 | /* Horizontal layout;
44 | *
45 | */
46 | .noUi-pips-horizontal {
47 | padding: 0;
48 | height: 20px;
49 | top: 0%;
50 | left: 0;
51 | width: 100%;
52 | }
53 | .noUi-value-horizontal {
54 | -webkit-transform: translate3d(-50%,50%,0);
55 | transform: translate3d(-50%,50%,0);
56 | }
57 |
58 | .noUi-marker-horizontal.noUi-marker {
59 | width: 1px;
60 | height: 5px;
61 | }
62 | .noUi-marker-horizontal.noUi-marker-sub {
63 | height: 18px;
64 | }
65 | .noUi-marker-horizontal.noUi-marker-large {
66 | height: 15px;
67 | }
68 |
69 | /* Vertical layout;
70 | *
71 | */
72 | .noUi-pips-vertical {
73 | padding: 0 10px;
74 | height: 100%;
75 | top: 0;
76 | left: 100%;
77 | }
78 | .noUi-value-vertical {
79 | -webkit-transform: translate3d(0,50%,0);
80 | transform: translate3d(0,50%,0);
81 | padding-left: 25px;
82 | }
83 |
84 | .noUi-marker-vertical.noUi-marker {
85 | width: 5px;
86 | height: 2px;
87 | margin-top: -1px;
88 | }
89 | .noUi-marker-vertical.noUi-marker-sub {
90 | width: 10px;
91 | }
92 | .noUi-marker-vertical.noUi-marker-large {
93 | width: 15px;
94 | }
95 |
--------------------------------------------------------------------------------
/src/assetbundles/tools/dist/css/tools.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Entries fieldtype with inline search
3 | */
4 | .oddsandends-elementsearch .add { position: relative; z-index: 1; display: inline-block; width: 12em; }
5 |
6 | body.ltr .oddsandends-elementsearch .add .text { padding-right: 30px; }
7 | body.rtl .oddsandends-elementsearch .add .text { padding-left: 30px; }
8 |
9 | .oddsandends-elementsearch .add .spinner { position: absolute; top: 0; }
10 | body.ltr .oddsandends-elementsearch .add .spinner { right: 5px; }
11 | body.rtl .oddsandends-elementsearch .add .spinner { left: 5px; }
12 |
13 | body.ltr .oddsandends-elementsearch__menu ul li a { padding-left: 32px; }
14 | body.rtl .oddsandends-elementsearch__menu ul li a { padding-right: 32px; }
15 |
16 | body.ltr .oddsandends-elementsearch__menu ul li a .status { float: left; }
17 | body.rtl .oddsandends-elementsearch__menu ul li a .status { float: right; }
18 | body.ltr .oddsandends-elementsearch__menu ul li a .status { margin: 3px 0 0 -18px ; }
19 | body.rtl .oddsandends-elementsearch__menu ul li a .status { margin: 3px -18px 0 0 ; }
20 |
21 |
22 | /**
23 | * Hide delete icons in disabled fields
24 | */
25 | .oddsandends-disabledcategories .delete,
26 | .oddsandends-disabledelements .delete {
27 | display: none;
28 | }
29 |
30 |
31 | /**
32 | * Styling Author Instruction field
33 | */
34 | .oddsandends__author-instructions {
35 | background-color: #f1f1f1;
36 | border-left: 4px solid #FF5500;
37 | padding: 15px;
38 | font-size: 15px;
39 | }
40 |
--------------------------------------------------------------------------------
/src/assetbundles/tools/dist/js/nouislider.js:
--------------------------------------------------------------------------------
1 | /*! nouislider - 9.0.0 - 2016-09-29 21:44:02 */
2 |
3 | (function (factory) {
4 |
5 | if ( typeof define === 'function' && define.amd ) {
6 |
7 | // AMD. Register as an anonymous module.
8 | define([], factory);
9 |
10 | } else if ( typeof exports === 'object' ) {
11 |
12 | // Node/CommonJS
13 | module.exports = factory();
14 |
15 | } else {
16 |
17 | // Browser globals
18 | window.noUiSlider = factory();
19 | }
20 |
21 | }(function( ){
22 |
23 | 'use strict';
24 |
25 |
26 | // Creates a node, adds it to target, returns the new node.
27 | function addNodeTo ( target, className ) {
28 | var div = document.createElement('div');
29 | addClass(div, className);
30 | target.appendChild(div);
31 | return div;
32 | }
33 |
34 | // Removes duplicates from an array.
35 | function unique ( array ) {
36 | return array.filter(function(a){
37 | return !this[a] ? this[a] = true : false;
38 | }, {});
39 | }
40 |
41 | // Round a value to the closest 'to'.
42 | function closest ( value, to ) {
43 | return Math.round(value / to) * to;
44 | }
45 |
46 | // Current position of an element relative to the document.
47 | function offset ( elem, orientation ) {
48 |
49 | var rect = elem.getBoundingClientRect(),
50 | doc = elem.ownerDocument,
51 | docElem = doc.documentElement,
52 | pageOffset = getPageOffset();
53 |
54 | // getBoundingClientRect contains left scroll in Chrome on Android.
55 | // I haven't found a feature detection that proves this. Worst case
56 | // scenario on mis-match: the 'tap' feature on horizontal sliders breaks.
57 | if ( /webkit.*Chrome.*Mobile/i.test(navigator.userAgent) ) {
58 | pageOffset.x = 0;
59 | }
60 |
61 | return orientation ? (rect.top + pageOffset.y - docElem.clientTop) : (rect.left + pageOffset.x - docElem.clientLeft);
62 | }
63 |
64 | // Checks whether a value is numerical.
65 | function isNumeric ( a ) {
66 | return typeof a === 'number' && !isNaN( a ) && isFinite( a );
67 | }
68 |
69 | // Sets a class and removes it after [duration] ms.
70 | function addClassFor ( element, className, duration ) {
71 | if (duration > 0) {
72 | addClass(element, className);
73 | setTimeout(function(){
74 | removeClass(element, className);
75 | }, duration);
76 | }
77 | }
78 |
79 | // Limits a value to 0 - 100
80 | function limit ( a ) {
81 | return Math.max(Math.min(a, 100), 0);
82 | }
83 |
84 | // Wraps a variable as an array, if it isn't one yet.
85 | // Note that an input array is returned by reference!
86 | function asArray ( a ) {
87 | return Array.isArray(a) ? a : [a];
88 | }
89 |
90 | // Counts decimals
91 | function countDecimals ( numStr ) {
92 | numStr = String(numStr);
93 | var pieces = numStr.split(".");
94 | return pieces.length > 1 ? pieces[1].length : 0;
95 | }
96 |
97 | // http://youmightnotneedjquery.com/#add_class
98 | function addClass ( el, className ) {
99 | if ( el.classList ) {
100 | el.classList.add(className);
101 | } else {
102 | el.className += ' ' + className;
103 | }
104 | }
105 |
106 | // http://youmightnotneedjquery.com/#remove_class
107 | function removeClass ( el, className ) {
108 | if ( el.classList ) {
109 | el.classList.remove(className);
110 | } else {
111 | el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
112 | }
113 | }
114 |
115 | // https://plainjs.com/javascript/attributes/adding-removing-and-testing-for-classes-9/
116 | function hasClass ( el, className ) {
117 | return el.classList ? el.classList.contains(className) : new RegExp('\\b' + className + '\\b').test(el.className);
118 | }
119 |
120 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY#Notes
121 | function getPageOffset ( ) {
122 |
123 | var supportPageOffset = window.pageXOffset !== undefined,
124 | isCSS1Compat = ((document.compatMode || "") === "CSS1Compat"),
125 | x = supportPageOffset ? window.pageXOffset : isCSS1Compat ? document.documentElement.scrollLeft : document.body.scrollLeft,
126 | y = supportPageOffset ? window.pageYOffset : isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop;
127 |
128 | return {
129 | x: x,
130 | y: y
131 | };
132 | }
133 |
134 | // we provide a function to compute constants instead
135 | // of accessing window.* as soon as the module needs it
136 | // so that we do not compute anything if not needed
137 | function getActions ( ) {
138 |
139 | // Determine the events to bind. IE11 implements pointerEvents without
140 | // a prefix, which breaks compatibility with the IE10 implementation.
141 | return window.navigator.pointerEnabled ? {
142 | start: 'pointerdown',
143 | move: 'pointermove',
144 | end: 'pointerup'
145 | } : window.navigator.msPointerEnabled ? {
146 | start: 'MSPointerDown',
147 | move: 'MSPointerMove',
148 | end: 'MSPointerUp'
149 | } : {
150 | start: 'mousedown touchstart',
151 | move: 'mousemove touchmove',
152 | end: 'mouseup touchend'
153 | };
154 | }
155 |
156 |
157 | // Value calculation
158 |
159 | // Determine the size of a sub-range in relation to a full range.
160 | function subRangeRatio ( pa, pb ) {
161 | return (100 / (pb - pa));
162 | }
163 |
164 | // (percentage) How many percent is this value of this range?
165 | function fromPercentage ( range, value ) {
166 | return (value * 100) / ( range[1] - range[0] );
167 | }
168 |
169 | // (percentage) Where is this value on this range?
170 | function toPercentage ( range, value ) {
171 | return fromPercentage( range, range[0] < 0 ?
172 | value + Math.abs(range[0]) :
173 | value - range[0] );
174 | }
175 |
176 | // (value) How much is this percentage on this range?
177 | function isPercentage ( range, value ) {
178 | return ((value * ( range[1] - range[0] )) / 100) + range[0];
179 | }
180 |
181 |
182 | // Range conversion
183 |
184 | function getJ ( value, arr ) {
185 |
186 | var j = 1;
187 |
188 | while ( value >= arr[j] ){
189 | j += 1;
190 | }
191 |
192 | return j;
193 | }
194 |
195 | // (percentage) Input a value, find where, on a scale of 0-100, it applies.
196 | function toStepping ( xVal, xPct, value ) {
197 |
198 | if ( value >= xVal.slice(-1)[0] ){
199 | return 100;
200 | }
201 |
202 | var j = getJ( value, xVal ), va, vb, pa, pb;
203 |
204 | va = xVal[j-1];
205 | vb = xVal[j];
206 | pa = xPct[j-1];
207 | pb = xPct[j];
208 |
209 | return pa + (toPercentage([va, vb], value) / subRangeRatio (pa, pb));
210 | }
211 |
212 | // (value) Input a percentage, find where it is on the specified range.
213 | function fromStepping ( xVal, xPct, value ) {
214 |
215 | // There is no range group that fits 100
216 | if ( value >= 100 ){
217 | return xVal.slice(-1)[0];
218 | }
219 |
220 | var j = getJ( value, xPct ), va, vb, pa, pb;
221 |
222 | va = xVal[j-1];
223 | vb = xVal[j];
224 | pa = xPct[j-1];
225 | pb = xPct[j];
226 |
227 | return isPercentage([va, vb], (value - pa) * subRangeRatio (pa, pb));
228 | }
229 |
230 | // (percentage) Get the step that applies at a certain value.
231 | function getStep ( xPct, xSteps, snap, value ) {
232 |
233 | if ( value === 100 ) {
234 | return value;
235 | }
236 |
237 | var j = getJ( value, xPct ), a, b;
238 |
239 | // If 'snap' is set, steps are used as fixed points on the slider.
240 | if ( snap ) {
241 |
242 | a = xPct[j-1];
243 | b = xPct[j];
244 |
245 | // Find the closest position, a or b.
246 | if ((value - a) > ((b-a)/2)){
247 | return b;
248 | }
249 |
250 | return a;
251 | }
252 |
253 | if ( !xSteps[j-1] ){
254 | return value;
255 | }
256 |
257 | return xPct[j-1] + closest(
258 | value - xPct[j-1],
259 | xSteps[j-1]
260 | );
261 | }
262 |
263 |
264 | // Entry parsing
265 |
266 | function handleEntryPoint ( index, value, that ) {
267 |
268 | var percentage;
269 |
270 | // Wrap numerical input in an array.
271 | if ( typeof value === "number" ) {
272 | value = [value];
273 | }
274 |
275 | // Reject any invalid input, by testing whether value is an array.
276 | if ( Object.prototype.toString.call( value ) !== '[object Array]' ){
277 | throw new Error("noUiSlider: 'range' contains invalid value.");
278 | }
279 |
280 | // Covert min/max syntax to 0 and 100.
281 | if ( index === 'min' ) {
282 | percentage = 0;
283 | } else if ( index === 'max' ) {
284 | percentage = 100;
285 | } else {
286 | percentage = parseFloat( index );
287 | }
288 |
289 | // Check for correct input.
290 | if ( !isNumeric( percentage ) || !isNumeric( value[0] ) ) {
291 | throw new Error("noUiSlider: 'range' value isn't numeric.");
292 | }
293 |
294 | // Store values.
295 | that.xPct.push( percentage );
296 | that.xVal.push( value[0] );
297 |
298 | // NaN will evaluate to false too, but to keep
299 | // logging clear, set step explicitly. Make sure
300 | // not to override the 'step' setting with false.
301 | if ( !percentage ) {
302 | if ( !isNaN( value[1] ) ) {
303 | that.xSteps[0] = value[1];
304 | }
305 | } else {
306 | that.xSteps.push( isNaN(value[1]) ? false : value[1] );
307 | }
308 |
309 | that.xHighestCompleteStep.push(0);
310 | }
311 |
312 | function handleStepPoint ( i, n, that ) {
313 |
314 | // Ignore 'false' stepping.
315 | if ( !n ) {
316 | return true;
317 | }
318 |
319 | // Factor to range ratio
320 | that.xSteps[i] = fromPercentage([
321 | that.xVal[i]
322 | ,that.xVal[i+1]
323 | ], n) / subRangeRatio (
324 | that.xPct[i],
325 | that.xPct[i+1] );
326 |
327 | var totalSteps = (that.xVal[i+1] - that.xVal[i]) / that.xNumSteps[i];
328 | var highestStep = Math.ceil(Number(totalSteps.toFixed(3)) - 1);
329 | var step = that.xVal[i] + (that.xNumSteps[i] * highestStep);
330 |
331 | that.xHighestCompleteStep[i] = step;
332 | }
333 |
334 |
335 | // Interface
336 |
337 | // The interface to Spectrum handles all direction-based
338 | // conversions, so the above values are unaware.
339 |
340 | function Spectrum ( entry, snap, direction, singleStep ) {
341 |
342 | this.xPct = [];
343 | this.xVal = [];
344 | this.xSteps = [ singleStep || false ];
345 | this.xNumSteps = [ false ];
346 | this.xHighestCompleteStep = [];
347 |
348 | this.snap = snap;
349 | this.direction = direction;
350 |
351 | var index, ordered = [ /* [0, 'min'], [1, '50%'], [2, 'max'] */ ];
352 |
353 | // Map the object keys to an array.
354 | for ( index in entry ) {
355 | if ( entry.hasOwnProperty(index) ) {
356 | ordered.push([entry[index], index]);
357 | }
358 | }
359 |
360 | // Sort all entries by value (numeric sort).
361 | if ( ordered.length && typeof ordered[0][0] === "object" ) {
362 | ordered.sort(function(a, b) { return a[0][0] - b[0][0]; });
363 | } else {
364 | ordered.sort(function(a, b) { return a[0] - b[0]; });
365 | }
366 |
367 |
368 | // Convert all entries to subranges.
369 | for ( index = 0; index < ordered.length; index++ ) {
370 | handleEntryPoint(ordered[index][1], ordered[index][0], this);
371 | }
372 |
373 | // Store the actual step values.
374 | // xSteps is sorted in the same order as xPct and xVal.
375 | this.xNumSteps = this.xSteps.slice(0);
376 |
377 | // Convert all numeric steps to the percentage of the subrange they represent.
378 | for ( index = 0; index < this.xNumSteps.length; index++ ) {
379 | handleStepPoint(index, this.xNumSteps[index], this);
380 | }
381 | }
382 |
383 | Spectrum.prototype.getMargin = function ( value ) {
384 |
385 | var step = this.xNumSteps[0];
386 |
387 | if ( step && (value % step) ) {
388 | throw new Error("noUiSlider: 'limit' and 'margin' must be divisible by step.");
389 | }
390 |
391 | return this.xPct.length === 2 ? fromPercentage(this.xVal, value) : false;
392 | };
393 |
394 | Spectrum.prototype.toStepping = function ( value ) {
395 |
396 | value = toStepping( this.xVal, this.xPct, value );
397 |
398 | return value;
399 | };
400 |
401 | Spectrum.prototype.fromStepping = function ( value ) {
402 |
403 | return fromStepping( this.xVal, this.xPct, value );
404 | };
405 |
406 | Spectrum.prototype.getStep = function ( value ) {
407 |
408 | value = getStep(this.xPct, this.xSteps, this.snap, value );
409 |
410 | return value;
411 | };
412 |
413 | Spectrum.prototype.getNearbySteps = function ( value ) {
414 |
415 | var j = getJ(value, this.xPct);
416 |
417 | return {
418 | stepBefore: { startValue: this.xVal[j-2], step: this.xNumSteps[j-2], highestStep: this.xHighestCompleteStep[j-2] },
419 | thisStep: { startValue: this.xVal[j-1], step: this.xNumSteps[j-1], highestStep: this.xHighestCompleteStep[j-1] },
420 | stepAfter: { startValue: this.xVal[j-0], step: this.xNumSteps[j-0], highestStep: this.xHighestCompleteStep[j-0] }
421 | };
422 | };
423 |
424 | Spectrum.prototype.countStepDecimals = function () {
425 | var stepDecimals = this.xNumSteps.map(countDecimals);
426 | return Math.max.apply(null, stepDecimals);
427 | };
428 |
429 | // Outside testing
430 | Spectrum.prototype.convert = function ( value ) {
431 | return this.getStep(this.toStepping(value));
432 | };
433 |
434 | /* Every input option is tested and parsed. This'll prevent
435 | endless validation in internal methods. These tests are
436 | structured with an item for every option available. An
437 | option can be marked as required by setting the 'r' flag.
438 | The testing function is provided with three arguments:
439 | - The provided value for the option;
440 | - A reference to the options object;
441 | - The name for the option;
442 |
443 | The testing function returns false when an error is detected,
444 | or true when everything is OK. It can also modify the option
445 | object, to make sure all values can be correctly looped elsewhere. */
446 |
447 | var defaultFormatter = { 'to': function( value ){
448 | return value !== undefined && value.toFixed(2);
449 | }, 'from': Number };
450 |
451 | function testStep ( parsed, entry ) {
452 |
453 | if ( !isNumeric( entry ) ) {
454 | throw new Error("noUiSlider: 'step' is not numeric.");
455 | }
456 |
457 | // The step option can still be used to set stepping
458 | // for linear sliders. Overwritten if set in 'range'.
459 | parsed.singleStep = entry;
460 | }
461 |
462 | function testRange ( parsed, entry ) {
463 |
464 | // Filter incorrect input.
465 | if ( typeof entry !== 'object' || Array.isArray(entry) ) {
466 | throw new Error("noUiSlider: 'range' is not an object.");
467 | }
468 |
469 | // Catch missing start or end.
470 | if ( entry.min === undefined || entry.max === undefined ) {
471 | throw new Error("noUiSlider: Missing 'min' or 'max' in 'range'.");
472 | }
473 |
474 | // Catch equal start or end.
475 | if ( entry.min === entry.max ) {
476 | throw new Error("noUiSlider: 'range' 'min' and 'max' cannot be equal.");
477 | }
478 |
479 | parsed.spectrum = new Spectrum(entry, parsed.snap, parsed.dir, parsed.singleStep);
480 | }
481 |
482 | function testStart ( parsed, entry ) {
483 |
484 | entry = asArray(entry);
485 |
486 | // Validate input. Values aren't tested, as the public .val method
487 | // will always provide a valid location.
488 | if ( !Array.isArray( entry ) || !entry.length ) {
489 | throw new Error("noUiSlider: 'start' option is incorrect.");
490 | }
491 |
492 | // Store the number of handles.
493 | parsed.handles = entry.length;
494 |
495 | // When the slider is initialized, the .val method will
496 | // be called with the start options.
497 | parsed.start = entry;
498 | }
499 |
500 | function testSnap ( parsed, entry ) {
501 |
502 | // Enforce 100% stepping within subranges.
503 | parsed.snap = entry;
504 |
505 | if ( typeof entry !== 'boolean' ){
506 | throw new Error("noUiSlider: 'snap' option must be a boolean.");
507 | }
508 | }
509 |
510 | function testAnimate ( parsed, entry ) {
511 |
512 | // Enforce 100% stepping within subranges.
513 | parsed.animate = entry;
514 |
515 | if ( typeof entry !== 'boolean' ){
516 | throw new Error("noUiSlider: 'animate' option must be a boolean.");
517 | }
518 | }
519 |
520 | function testAnimationDuration ( parsed, entry ) {
521 |
522 | parsed.animationDuration = entry;
523 |
524 | if ( typeof entry !== 'number' ){
525 | throw new Error("noUiSlider: 'animationDuration' option must be a number.");
526 | }
527 | }
528 |
529 | function testConnect ( parsed, entry ) {
530 |
531 | var connect = [false];
532 | var i;
533 |
534 | if ( entry === true || entry === false ) {
535 |
536 | for ( i = 1; i < parsed.handles; i++ ) {
537 | connect.push(entry);
538 | }
539 |
540 | connect.push(false);
541 | }
542 |
543 | else if ( !Array.isArray( entry ) || !entry.length || entry.length !== parsed.handles + 1 ) {
544 | throw new Error("noUiSlider: 'connect' option doesn't match handle count.");
545 | }
546 |
547 | else {
548 | connect = entry;
549 | }
550 |
551 | parsed.connect = connect;
552 | }
553 |
554 | function testOrientation ( parsed, entry ) {
555 |
556 | // Set orientation to an a numerical value for easy
557 | // array selection.
558 | switch ( entry ){
559 | case 'horizontal':
560 | parsed.ort = 0;
561 | break;
562 | case 'vertical':
563 | parsed.ort = 1;
564 | break;
565 | default:
566 | throw new Error("noUiSlider: 'orientation' option is invalid.");
567 | }
568 | }
569 |
570 | function testMargin ( parsed, entry ) {
571 |
572 | if ( !isNumeric(entry) ){
573 | throw new Error("noUiSlider: 'margin' option must be numeric.");
574 | }
575 |
576 | // Issue #582
577 | if ( entry === 0 ) {
578 | return;
579 | }
580 |
581 | parsed.margin = parsed.spectrum.getMargin(entry);
582 |
583 | if ( !parsed.margin ) {
584 | throw new Error("noUiSlider: 'margin' option is only supported on linear sliders.");
585 | }
586 | }
587 |
588 | function testLimit ( parsed, entry ) {
589 |
590 | if ( !isNumeric(entry) ){
591 | throw new Error("noUiSlider: 'limit' option must be numeric.");
592 | }
593 |
594 | parsed.limit = parsed.spectrum.getMargin(entry);
595 |
596 | if ( !parsed.limit || parsed.handles < 2 ) {
597 | throw new Error("noUiSlider: 'limit' option is only supported on linear sliders with 2 or more handles.");
598 | }
599 | }
600 |
601 | function testDirection ( parsed, entry ) {
602 |
603 | // Set direction as a numerical value for easy parsing.
604 | // Invert connection for RTL sliders, so that the proper
605 | // handles get the connect/background classes.
606 | switch ( entry ) {
607 | case 'ltr':
608 | parsed.dir = 0;
609 | break;
610 | case 'rtl':
611 | parsed.dir = 1;
612 | break;
613 | default:
614 | throw new Error("noUiSlider: 'direction' option was not recognized.");
615 | }
616 | }
617 |
618 | function testBehaviour ( parsed, entry ) {
619 |
620 | // Make sure the input is a string.
621 | if ( typeof entry !== 'string' ) {
622 | throw new Error("noUiSlider: 'behaviour' must be a string containing options.");
623 | }
624 |
625 | // Check if the string contains any keywords.
626 | // None are required.
627 | var tap = entry.indexOf('tap') >= 0;
628 | var drag = entry.indexOf('drag') >= 0;
629 | var fixed = entry.indexOf('fixed') >= 0;
630 | var snap = entry.indexOf('snap') >= 0;
631 | var hover = entry.indexOf('hover') >= 0;
632 |
633 | if ( fixed ) {
634 |
635 | if ( parsed.handles !== 2 ) {
636 | throw new Error("noUiSlider: 'fixed' behaviour must be used with 2 handles");
637 | }
638 |
639 | // Use margin to enforce fixed state
640 | testMargin(parsed, parsed.start[1] - parsed.start[0]);
641 | }
642 |
643 | parsed.events = {
644 | tap: tap || snap,
645 | drag: drag,
646 | fixed: fixed,
647 | snap: snap,
648 | hover: hover
649 | };
650 | }
651 |
652 | function testTooltips ( parsed, entry ) {
653 |
654 | if ( entry === false ) {
655 | return;
656 | }
657 |
658 | else if ( entry === true ) {
659 |
660 | parsed.tooltips = [];
661 |
662 | for ( var i = 0; i < parsed.handles; i++ ) {
663 | parsed.tooltips.push(true);
664 | }
665 | }
666 |
667 | else {
668 |
669 | parsed.tooltips = asArray(entry);
670 |
671 | if ( parsed.tooltips.length !== parsed.handles ) {
672 | throw new Error("noUiSlider: must pass a formatter for all handles.");
673 | }
674 |
675 | parsed.tooltips.forEach(function(formatter){
676 | if ( typeof formatter !== 'boolean' && (typeof formatter !== 'object' || typeof formatter.to !== 'function') ) {
677 | throw new Error("noUiSlider: 'tooltips' must be passed a formatter or 'false'.");
678 | }
679 | });
680 | }
681 | }
682 |
683 | function testFormat ( parsed, entry ) {
684 |
685 | parsed.format = entry;
686 |
687 | // Any object with a to and from method is supported.
688 | if ( typeof entry.to === 'function' && typeof entry.from === 'function' ) {
689 | return true;
690 | }
691 |
692 | throw new Error("noUiSlider: 'format' requires 'to' and 'from' methods.");
693 | }
694 |
695 | function testCssPrefix ( parsed, entry ) {
696 |
697 | if ( entry !== undefined && typeof entry !== 'string' && entry !== false ) {
698 | throw new Error("noUiSlider: 'cssPrefix' must be a string or `false`.");
699 | }
700 |
701 | parsed.cssPrefix = entry;
702 | }
703 |
704 | function testCssClasses ( parsed, entry ) {
705 |
706 | if ( entry !== undefined && typeof entry !== 'object' ) {
707 | throw new Error("noUiSlider: 'cssClasses' must be an object.");
708 | }
709 |
710 | if ( typeof parsed.cssPrefix === 'string' ) {
711 | parsed.cssClasses = {};
712 |
713 | for ( var key in entry ) {
714 | if ( !entry.hasOwnProperty(key) ) { continue; }
715 |
716 | parsed.cssClasses[key] = parsed.cssPrefix + entry[key];
717 | }
718 | } else {
719 | parsed.cssClasses = entry;
720 | }
721 | }
722 |
723 | function testUseRaf ( parsed, entry ) {
724 | if ( entry === true || entry === false ) {
725 | parsed.useRequestAnimationFrame = entry;
726 | } else {
727 | throw new Error("noUiSlider: 'useRequestAnimationFrame' option should be true (default) or false.");
728 | }
729 | }
730 |
731 | // Test all developer settings and parse to assumption-safe values.
732 | function testOptions ( options ) {
733 |
734 | // To prove a fix for #537, freeze options here.
735 | // If the object is modified, an error will be thrown.
736 | // Object.freeze(options);
737 |
738 | var parsed = {
739 | margin: 0,
740 | limit: 0,
741 | animate: true,
742 | animationDuration: 300,
743 | format: defaultFormatter
744 | }, tests;
745 |
746 | // Tests are executed in the order they are presented here.
747 | tests = {
748 | 'step': { r: false, t: testStep },
749 | 'start': { r: true, t: testStart },
750 | 'connect': { r: true, t: testConnect },
751 | 'direction': { r: true, t: testDirection },
752 | 'snap': { r: false, t: testSnap },
753 | 'animate': { r: false, t: testAnimate },
754 | 'animationDuration': { r: false, t: testAnimationDuration },
755 | 'range': { r: true, t: testRange },
756 | 'orientation': { r: false, t: testOrientation },
757 | 'margin': { r: false, t: testMargin },
758 | 'limit': { r: false, t: testLimit },
759 | 'behaviour': { r: true, t: testBehaviour },
760 | 'format': { r: false, t: testFormat },
761 | 'tooltips': { r: false, t: testTooltips },
762 | 'cssPrefix': { r: false, t: testCssPrefix },
763 | 'cssClasses': { r: false, t: testCssClasses },
764 | 'useRequestAnimationFrame': { r: false, t: testUseRaf }
765 | };
766 |
767 | var defaults = {
768 | 'connect': false,
769 | 'direction': 'ltr',
770 | 'behaviour': 'tap',
771 | 'orientation': 'horizontal',
772 | 'cssPrefix' : 'noUi-',
773 | 'cssClasses': {
774 | target: 'target',
775 | base: 'base',
776 | origin: 'origin',
777 | handle: 'handle',
778 | horizontal: 'horizontal',
779 | vertical: 'vertical',
780 | background: 'background',
781 | connect: 'connect',
782 | ltr: 'ltr',
783 | rtl: 'rtl',
784 | draggable: 'draggable',
785 | drag: 'state-drag',
786 | tap: 'state-tap',
787 | active: 'active',
788 | tooltip: 'tooltip',
789 | pips: 'pips',
790 | pipsHorizontal: 'pips-horizontal',
791 | pipsVertical: 'pips-vertical',
792 | marker: 'marker',
793 | markerHorizontal: 'marker-horizontal',
794 | markerVertical: 'marker-vertical',
795 | markerNormal: 'marker-normal',
796 | markerLarge: 'marker-large',
797 | markerSub: 'marker-sub',
798 | value: 'value',
799 | valueHorizontal: 'value-horizontal',
800 | valueVertical: 'value-vertical',
801 | valueNormal: 'value-normal',
802 | valueLarge: 'value-large',
803 | valueSub: 'value-sub'
804 | },
805 | 'useRequestAnimationFrame': true
806 | };
807 |
808 | // Run all options through a testing mechanism to ensure correct
809 | // input. It should be noted that options might get modified to
810 | // be handled properly. E.g. wrapping integers in arrays.
811 | Object.keys(tests).forEach(function( name ){
812 |
813 | // If the option isn't set, but it is required, throw an error.
814 | if ( options[name] === undefined && defaults[name] === undefined ) {
815 |
816 | if ( tests[name].r ) {
817 | throw new Error("noUiSlider: '" + name + "' is required.");
818 | }
819 |
820 | return true;
821 | }
822 |
823 | tests[name].t( parsed, options[name] === undefined ? defaults[name] : options[name] );
824 | });
825 |
826 | // Forward pips options
827 | parsed.pips = options.pips;
828 |
829 | var styles = [['left', 'top'], ['right', 'bottom']];
830 |
831 | // Pre-define the styles.
832 | parsed.style = styles[parsed.dir][parsed.ort];
833 | parsed.styleOposite = styles[parsed.dir?0:1][parsed.ort];
834 |
835 | return parsed;
836 | }
837 |
838 |
839 | function closure ( target, options, originalOptions ){
840 |
841 | var actions = getActions( );
842 |
843 | // All variables local to 'closure' are prefixed with 'scope_'
844 | var scope_Target = target;
845 | var scope_Locations = [];
846 | var scope_Base;
847 | var scope_Handles;
848 | var scope_HandleNumbers = [];
849 | var scope_Connects;
850 | var scope_Spectrum = options.spectrum;
851 | var scope_Values = [];
852 | var scope_Events = {};
853 | var scope_Self;
854 |
855 |
856 | // Append a origin to the base
857 | function addOrigin ( base, handleNumber ) {
858 | var origin = addNodeTo(base, options.cssClasses.origin);
859 | var handle = addNodeTo(origin, options.cssClasses.handle);
860 | handle.setAttribute('data-handle', handleNumber);
861 | return origin;
862 | }
863 |
864 | // Insert nodes for connect elements
865 | function addConnect ( base, add ) {
866 |
867 | if ( !add ) {
868 | return false;
869 | }
870 |
871 | return addNodeTo(base, options.cssClasses.connect);
872 | }
873 |
874 | // Add handles to the slider base.
875 | function addElements ( connectOptions, base ) {
876 |
877 | scope_Handles = [];
878 | scope_Connects = [];
879 |
880 | scope_Connects.push(addConnect(base, connectOptions[0]));
881 |
882 | // [::::O====O====O====]
883 | // connectOptions = [0, 1, 1, 1]
884 |
885 | for ( var i = 0; i < options.handles; i++ ) {
886 | // Keep a list of all added handles.
887 | scope_Handles.push(addOrigin(base, i));
888 | scope_HandleNumbers[i] = i;
889 | scope_Connects.push(addConnect(base, connectOptions[i + 1]));
890 | }
891 | }
892 |
893 | // Initialize a single slider.
894 | function addSlider ( target ) {
895 |
896 | // Apply classes and data to the target.
897 | addClass(target, options.cssClasses.target);
898 |
899 | if ( options.dir === 0 ) {
900 | addClass(target, options.cssClasses.ltr);
901 | } else {
902 | addClass(target, options.cssClasses.rtl);
903 | }
904 |
905 | if ( options.ort === 0 ) {
906 | addClass(target, options.cssClasses.horizontal);
907 | } else {
908 | addClass(target, options.cssClasses.vertical);
909 | }
910 |
911 | scope_Base = addNodeTo(target, options.cssClasses.base);
912 | }
913 |
914 |
915 | function addTooltip ( handle, handleNumber ) {
916 |
917 | if ( !options.tooltips[handleNumber] ) {
918 | return false;
919 | }
920 |
921 | return addNodeTo(handle.firstChild, options.cssClasses.tooltip);
922 | }
923 |
924 | // The tooltips option is a shorthand for using the 'update' event.
925 | function tooltips ( ) {
926 |
927 | // Tooltips are added with options.tooltips in original order.
928 | var tips = scope_Handles.map(addTooltip);
929 |
930 | bindEvent('update', function(values, handleNumber, unencoded) {
931 |
932 | if ( !tips[handleNumber] ) {
933 | return;
934 | }
935 |
936 | var formattedValue = values[handleNumber];
937 |
938 | if ( options.tooltips[handleNumber] !== true ) {
939 | formattedValue = options.tooltips[handleNumber].to(unencoded[handleNumber]);
940 | }
941 |
942 | tips[handleNumber].innerHTML = formattedValue;
943 | });
944 | }
945 |
946 |
947 | function getGroup ( mode, values, stepped ) {
948 |
949 | // Use the range.
950 | if ( mode === 'range' || mode === 'steps' ) {
951 | return scope_Spectrum.xVal;
952 | }
953 |
954 | if ( mode === 'count' ) {
955 |
956 | // Divide 0 - 100 in 'count' parts.
957 | var spread = ( 100 / (values-1) ), v, i = 0;
958 | values = [];
959 |
960 | // List these parts and have them handled as 'positions'.
961 | while ((v=i++*spread) <= 100 ) {
962 | values.push(v);
963 | }
964 |
965 | mode = 'positions';
966 | }
967 |
968 | if ( mode === 'positions' ) {
969 |
970 | // Map all percentages to on-range values.
971 | return values.map(function( value ){
972 | return scope_Spectrum.fromStepping( stepped ? scope_Spectrum.getStep( value ) : value );
973 | });
974 | }
975 |
976 | if ( mode === 'values' ) {
977 |
978 | // If the value must be stepped, it needs to be converted to a percentage first.
979 | if ( stepped ) {
980 |
981 | return values.map(function( value ){
982 |
983 | // Convert to percentage, apply step, return to value.
984 | return scope_Spectrum.fromStepping( scope_Spectrum.getStep( scope_Spectrum.toStepping( value ) ) );
985 | });
986 |
987 | }
988 |
989 | // Otherwise, we can simply use the values.
990 | return values;
991 | }
992 | }
993 |
994 | function generateSpread ( density, mode, group ) {
995 |
996 | function safeIncrement(value, increment) {
997 | // Avoid floating point variance by dropping the smallest decimal places.
998 | return (value + increment).toFixed(7) / 1;
999 | }
1000 |
1001 | var indexes = {},
1002 | firstInRange = scope_Spectrum.xVal[0],
1003 | lastInRange = scope_Spectrum.xVal[scope_Spectrum.xVal.length-1],
1004 | ignoreFirst = false,
1005 | ignoreLast = false,
1006 | prevPct = 0;
1007 |
1008 | // Create a copy of the group, sort it and filter away all duplicates.
1009 | group = unique(group.slice().sort(function(a, b){ return a - b; }));
1010 |
1011 | // Make sure the range starts with the first element.
1012 | if ( group[0] !== firstInRange ) {
1013 | group.unshift(firstInRange);
1014 | ignoreFirst = true;
1015 | }
1016 |
1017 | // Likewise for the last one.
1018 | if ( group[group.length - 1] !== lastInRange ) {
1019 | group.push(lastInRange);
1020 | ignoreLast = true;
1021 | }
1022 |
1023 | group.forEach(function ( current, index ) {
1024 |
1025 | // Get the current step and the lower + upper positions.
1026 | var step, i, q,
1027 | low = current,
1028 | high = group[index+1],
1029 | newPct, pctDifference, pctPos, type,
1030 | steps, realSteps, stepsize;
1031 |
1032 | // When using 'steps' mode, use the provided steps.
1033 | // Otherwise, we'll step on to the next subrange.
1034 | if ( mode === 'steps' ) {
1035 | step = scope_Spectrum.xNumSteps[ index ];
1036 | }
1037 |
1038 | // Default to a 'full' step.
1039 | if ( !step ) {
1040 | step = high-low;
1041 | }
1042 |
1043 | // Low can be 0, so test for false. If high is undefined,
1044 | // we are at the last subrange. Index 0 is already handled.
1045 | if ( low === false || high === undefined ) {
1046 | return;
1047 | }
1048 |
1049 | // Make sure step isn't 0, which would cause an infinite loop (#654)
1050 | step = Math.max(step, 0.0000001);
1051 |
1052 | // Find all steps in the subrange.
1053 | for ( i = low; i <= high; i = safeIncrement(i, step) ) {
1054 |
1055 | // Get the percentage value for the current step,
1056 | // calculate the size for the subrange.
1057 | newPct = scope_Spectrum.toStepping( i );
1058 | pctDifference = newPct - prevPct;
1059 |
1060 | steps = pctDifference / density;
1061 | realSteps = Math.round(steps);
1062 |
1063 | // This ratio represents the ammount of percentage-space a point indicates.
1064 | // For a density 1 the points/percentage = 1. For density 2, that percentage needs to be re-devided.
1065 | // Round the percentage offset to an even number, then divide by two
1066 | // to spread the offset on both sides of the range.
1067 | stepsize = pctDifference/realSteps;
1068 |
1069 | // Divide all points evenly, adding the correct number to this subrange.
1070 | // Run up to <= so that 100% gets a point, event if ignoreLast is set.
1071 | for ( q = 1; q <= realSteps; q += 1 ) {
1072 |
1073 | // The ratio between the rounded value and the actual size might be ~1% off.
1074 | // Correct the percentage offset by the number of points
1075 | // per subrange. density = 1 will result in 100 points on the
1076 | // full range, 2 for 50, 4 for 25, etc.
1077 | pctPos = prevPct + ( q * stepsize );
1078 | indexes[pctPos.toFixed(5)] = ['x', 0];
1079 | }
1080 |
1081 | // Determine the point type.
1082 | type = (group.indexOf(i) > -1) ? 1 : ( mode === 'steps' ? 2 : 0 );
1083 |
1084 | // Enforce the 'ignoreFirst' option by overwriting the type for 0.
1085 | if ( !index && ignoreFirst ) {
1086 | type = 0;
1087 | }
1088 |
1089 | if ( !(i === high && ignoreLast)) {
1090 | // Mark the 'type' of this point. 0 = plain, 1 = real value, 2 = step value.
1091 | indexes[newPct.toFixed(5)] = [i, type];
1092 | }
1093 |
1094 | // Update the percentage count.
1095 | prevPct = newPct;
1096 | }
1097 | });
1098 |
1099 | return indexes;
1100 | }
1101 |
1102 | function addMarking ( spread, filterFunc, formatter ) {
1103 |
1104 | var element = document.createElement('div'),
1105 | out = '',
1106 | valueSizeClasses = [
1107 | options.cssClasses.valueNormal,
1108 | options.cssClasses.valueLarge,
1109 | options.cssClasses.valueSub
1110 | ],
1111 | markerSizeClasses = [
1112 | options.cssClasses.markerNormal,
1113 | options.cssClasses.markerLarge,
1114 | options.cssClasses.markerSub
1115 | ],
1116 | valueOrientationClasses = [
1117 | options.cssClasses.valueHorizontal,
1118 | options.cssClasses.valueVertical
1119 | ],
1120 | markerOrientationClasses = [
1121 | options.cssClasses.markerHorizontal,
1122 | options.cssClasses.markerVertical
1123 | ];
1124 |
1125 | addClass(element, options.cssClasses.pips);
1126 | addClass(element, options.ort === 0 ? options.cssClasses.pipsHorizontal : options.cssClasses.pipsVertical);
1127 |
1128 | function getClasses( type, source ){
1129 | var a = source === options.cssClasses.value,
1130 | orientationClasses = a ? valueOrientationClasses : markerOrientationClasses,
1131 | sizeClasses = a ? valueSizeClasses : markerSizeClasses;
1132 |
1133 | return source + ' ' + orientationClasses[options.ort] + ' ' + sizeClasses[type];
1134 | }
1135 |
1136 | function getTags( offset, source, values ) {
1137 | return 'class="' + getClasses(values[1], source) + '" style="' + options.style + ': ' + offset + '%"';
1138 | }
1139 |
1140 | function addSpread ( offset, values ){
1141 |
1142 | // Apply the filter function, if it is set.
1143 | values[1] = (values[1] && filterFunc) ? filterFunc(values[0], values[1]) : values[1];
1144 |
1145 | // Add a marker for every point
1146 | out += '';
1147 |
1148 | // Values are only appended for points marked '1' or '2'.
1149 | if ( values[1] ) {
1150 | out += '
' + formatter.to(values[0]) + '
';
1151 | }
1152 | }
1153 |
1154 | // Append all points.
1155 | Object.keys(spread).forEach(function(a){
1156 | addSpread(a, spread[a]);
1157 | });
1158 |
1159 | element.innerHTML = out;
1160 |
1161 | return element;
1162 | }
1163 |
1164 | function pips ( grid ) {
1165 |
1166 | var mode = grid.mode,
1167 | density = grid.density || 1,
1168 | filter = grid.filter || false,
1169 | values = grid.values || false,
1170 | stepped = grid.stepped || false,
1171 | group = getGroup( mode, values, stepped ),
1172 | spread = generateSpread( density, mode, group ),
1173 | format = grid.format || {
1174 | to: Math.round
1175 | };
1176 |
1177 | return scope_Target.appendChild(addMarking(
1178 | spread,
1179 | filter,
1180 | format
1181 | ));
1182 | }
1183 |
1184 |
1185 | // Shorthand for base dimensions.
1186 | function baseSize ( ) {
1187 | var rect = scope_Base.getBoundingClientRect(), alt = 'offset' + ['Width', 'Height'][options.ort];
1188 | return options.ort === 0 ? (rect.width||scope_Base[alt]) : (rect.height||scope_Base[alt]);
1189 | }
1190 |
1191 | // Handler for attaching events trough a proxy.
1192 | function attachEvent ( events, element, callback, data ) {
1193 |
1194 | // This function can be used to 'filter' events to the slider.
1195 | // element is a node, not a nodeList
1196 |
1197 | var method = function ( e ){
1198 |
1199 | if ( scope_Target.hasAttribute('disabled') ) {
1200 | return false;
1201 | }
1202 |
1203 | // Stop if an active 'tap' transition is taking place.
1204 | if ( hasClass(scope_Target, options.cssClasses.tap) ) {
1205 | return false;
1206 | }
1207 |
1208 | e = fixEvent(e, data.pageOffset);
1209 |
1210 | // Ignore right or middle clicks on start #454
1211 | if ( events === actions.start && e.buttons !== undefined && e.buttons > 1 ) {
1212 | return false;
1213 | }
1214 |
1215 | // Ignore right or middle clicks on start #454
1216 | if ( data.hover && e.buttons ) {
1217 | return false;
1218 | }
1219 |
1220 | e.calcPoint = e.points[ options.ort ];
1221 |
1222 | // Call the event handler with the event [ and additional data ].
1223 | callback ( e, data );
1224 | };
1225 |
1226 | var methods = [];
1227 |
1228 | // Bind a closure on the target for every event type.
1229 | events.split(' ').forEach(function( eventName ){
1230 | element.addEventListener(eventName, method, false);
1231 | methods.push([eventName, method]);
1232 | });
1233 |
1234 | return methods;
1235 | }
1236 |
1237 | // Provide a clean event with standardized offset values.
1238 | function fixEvent ( e, pageOffset ) {
1239 |
1240 | // Prevent scrolling and panning on touch events, while
1241 | // attempting to slide. The tap event also depends on this.
1242 | e.preventDefault();
1243 |
1244 | // Filter the event to register the type, which can be
1245 | // touch, mouse or pointer. Offset changes need to be
1246 | // made on an event specific basis.
1247 | var touch = e.type.indexOf('touch') === 0,
1248 | mouse = e.type.indexOf('mouse') === 0,
1249 | pointer = e.type.indexOf('pointer') === 0,
1250 | x,y, event = e;
1251 |
1252 | // IE10 implemented pointer events with a prefix;
1253 | if ( e.type.indexOf('MSPointer') === 0 ) {
1254 | pointer = true;
1255 | }
1256 |
1257 | if ( touch ) {
1258 |
1259 | // Fix bug when user touches with two or more fingers on mobile devices.
1260 | // It's useful when you have two or more sliders on one page,
1261 | // that can be touched simultaneously.
1262 | // #649, #663, #668
1263 | if ( event.touches.length > 1 ) {
1264 | return false;
1265 | }
1266 |
1267 | // noUiSlider supports one movement at a time,
1268 | // so we can select the first 'changedTouch'.
1269 | x = e.changedTouches[0].pageX;
1270 | y = e.changedTouches[0].pageY;
1271 | }
1272 |
1273 | pageOffset = pageOffset || getPageOffset();
1274 |
1275 | if ( mouse || pointer ) {
1276 | x = e.clientX + pageOffset.x;
1277 | y = e.clientY + pageOffset.y;
1278 | }
1279 |
1280 | event.pageOffset = pageOffset;
1281 | event.points = [x, y];
1282 | event.cursor = mouse || pointer; // Fix #435
1283 |
1284 | return event;
1285 | }
1286 |
1287 | function calcPointToPercentage ( calcPoint ) {
1288 | var location = calcPoint - offset(scope_Base, options.ort);
1289 | var proposal = ( location * 100 ) / baseSize();
1290 | return options.dir ? 100 - proposal : proposal;
1291 | }
1292 |
1293 | function getClosestHandle ( proposal ) {
1294 |
1295 | var closest = 100;
1296 | var handleNumber = false;
1297 |
1298 | scope_Handles.forEach(function(handle, index){
1299 |
1300 | // Disabled handles are ignored
1301 | if ( handle.hasAttribute('disabled') ) {
1302 | return;
1303 | }
1304 |
1305 | var pos = Math.abs(scope_Locations[index] - proposal);
1306 |
1307 | if ( pos < closest ) {
1308 | handleNumber = index;
1309 | closest = pos;
1310 | }
1311 | });
1312 |
1313 | return handleNumber;
1314 | }
1315 |
1316 | // Moves handle(s) by a percentage
1317 | // (bool, % to move, [% where handle started, ...], [index in scope_Handles, ...])
1318 | function moveHandles ( upward, proposal, locations, handleNumbers ) {
1319 |
1320 | var proposals = locations.slice();
1321 |
1322 | var b = [!upward, upward];
1323 | var f = [upward, !upward];
1324 |
1325 | // Copy handleNumbers so we don't change the dataset
1326 | handleNumbers = handleNumbers.slice();
1327 |
1328 | // Check to see which handle is 'leading'.
1329 | // If that one can't move the second can't either.
1330 | if ( upward ) {
1331 | handleNumbers.reverse();
1332 | }
1333 |
1334 | // Step 1: get the maximum percentage that any of the handles can move
1335 | if ( handleNumbers.length > 1 ) {
1336 |
1337 | handleNumbers.forEach(function(handleNumber, o) {
1338 |
1339 | var to = checkHandlePosition(proposals, handleNumber, proposals[handleNumber] + proposal, b[o], f[o]);
1340 |
1341 | // Stop if one of the handles can't move.
1342 | if ( to === false ) {
1343 | proposal = 0;
1344 | } else {
1345 | proposal = to - proposals[handleNumber];
1346 | proposals[handleNumber] = to;
1347 | }
1348 | });
1349 | }
1350 |
1351 | // If using one handle, check backward AND forward
1352 | else {
1353 | b = f = [true];
1354 | }
1355 |
1356 | var state = false;
1357 |
1358 | // Step 2: Try to set the handles with the found percentage
1359 | handleNumbers.forEach(function(handleNumber, o) {
1360 | state = setHandle(handleNumber, locations[handleNumber] + proposal, b[o], f[o]) || state;
1361 | });
1362 |
1363 | // Step 3: If a handle moved, fire events
1364 | if ( state ) {
1365 | handleNumbers.forEach(function(handleNumber){
1366 | fireEvent('update', handleNumber);
1367 | fireEvent('slide', handleNumber);
1368 | });
1369 | }
1370 | }
1371 |
1372 | // External event handling
1373 | function fireEvent ( eventName, handleNumber, tap ) {
1374 |
1375 | Object.keys(scope_Events).forEach(function( targetEvent ) {
1376 |
1377 | var eventType = targetEvent.split('.')[0];
1378 |
1379 | if ( eventName === eventType ) {
1380 | scope_Events[targetEvent].forEach(function( callback ) {
1381 |
1382 | callback.call(
1383 | // Use the slider public API as the scope ('this')
1384 | scope_Self,
1385 | // Return values as array, so arg_1[arg_2] is always valid.
1386 | scope_Values.map(options.format.to),
1387 | // Handle index, 0 or 1
1388 | handleNumber,
1389 | // Unformatted slider values
1390 | scope_Values.slice(),
1391 | // Event is fired by tap, true or false
1392 | tap || false,
1393 | // Left offset of the handle, in relation to the slider
1394 | scope_Locations.slice()
1395 | );
1396 | });
1397 | }
1398 | });
1399 | }
1400 |
1401 |
1402 | // Fire 'end' when a mouse or pen leaves the document.
1403 | function documentLeave ( event, data ) {
1404 | if ( event.type === "mouseout" && event.target.nodeName === "HTML" && event.relatedTarget === null ){
1405 | eventEnd (event, data);
1406 | }
1407 | }
1408 |
1409 | // Handle movement on document for handle and range drag.
1410 | function eventMove ( event, data ) {
1411 |
1412 | // Fix #498
1413 | // Check value of .buttons in 'start' to work around a bug in IE10 mobile (data.buttonsProperty).
1414 | // https://connect.microsoft.com/IE/feedback/details/927005/mobile-ie10-windows-phone-buttons-property-of-pointermove-event-always-zero
1415 | // IE9 has .buttons and .which zero on mousemove.
1416 | // Firefox breaks the spec MDN defines.
1417 | if ( navigator.appVersion.indexOf("MSIE 9") === -1 && event.buttons === 0 && data.buttonsProperty !== 0 ) {
1418 | return eventEnd(event, data);
1419 | }
1420 |
1421 | // Check if we are moving up or down
1422 | var movement = (options.dir ? -1 : 1) * (event.calcPoint - data.startCalcPoint);
1423 |
1424 | // Convert the movement into a percentage of the slider width/height
1425 | var proposal = (movement * 100) / data.baseSize;
1426 |
1427 | moveHandles(movement > 0, proposal, data.locations, data.handleNumbers);
1428 | }
1429 |
1430 | // Unbind move events on document, call callbacks.
1431 | function eventEnd ( event, data ) {
1432 |
1433 | // The handle is no longer active, so remove the class.
1434 | var active = scope_Base.querySelector( '.' + options.cssClasses.active );
1435 |
1436 | if ( active !== null ) {
1437 | removeClass(active, options.cssClasses.active);
1438 | }
1439 |
1440 | // Remove cursor styles and text-selection events bound to the body.
1441 | if ( event.cursor ) {
1442 | document.body.style.cursor = '';
1443 | document.body.removeEventListener('selectstart', document.body.noUiListener);
1444 | }
1445 |
1446 | // Unbind the move and end events, which are added on 'start'.
1447 | document.documentElement.noUiListeners.forEach(function( c ) {
1448 | document.documentElement.removeEventListener(c[0], c[1]);
1449 | });
1450 |
1451 | // Remove dragging class.
1452 | removeClass(scope_Target, options.cssClasses.drag);
1453 |
1454 | setZindex();
1455 |
1456 | data.handleNumbers.forEach(function(handleNumber){
1457 | fireEvent('set', handleNumber);
1458 | fireEvent('change', handleNumber);
1459 | fireEvent('end', handleNumber);
1460 | });
1461 | }
1462 |
1463 | // Bind move events on document.
1464 | function eventStart ( event, data ) {
1465 |
1466 | // Mark the handle as 'active' so it can be styled.
1467 | if ( data.handleNumbers.length === 1 ) {
1468 |
1469 | var handle = scope_Handles[data.handleNumbers[0]];
1470 |
1471 | // Ignore 'disabled' handles
1472 | if ( handle.hasAttribute('disabled') ) {
1473 | return false;
1474 | }
1475 |
1476 | addClass(handle.children[0], options.cssClasses.active);
1477 | }
1478 |
1479 | // Fix #551, where a handle gets selected instead of dragged.
1480 | event.preventDefault();
1481 |
1482 | // A drag should never propagate up to the 'tap' event.
1483 | event.stopPropagation();
1484 |
1485 | // Attach the move and end events.
1486 | var moveEvent = attachEvent(actions.move, document.documentElement, eventMove, {
1487 | startCalcPoint: event.calcPoint,
1488 | baseSize: baseSize(),
1489 | pageOffset: event.pageOffset,
1490 | handleNumbers: data.handleNumbers,
1491 | buttonsProperty: event.buttons,
1492 | locations: scope_Locations.slice()
1493 | });
1494 |
1495 | var endEvent = attachEvent(actions.end, document.documentElement, eventEnd, {
1496 | handleNumbers: data.handleNumbers
1497 | });
1498 |
1499 | var outEvent = attachEvent("mouseout", document.documentElement, documentLeave, {
1500 | handleNumbers: data.handleNumbers
1501 | });
1502 |
1503 | document.documentElement.noUiListeners = moveEvent.concat(endEvent, outEvent);
1504 |
1505 | // Text selection isn't an issue on touch devices,
1506 | // so adding cursor styles can be skipped.
1507 | if ( event.cursor ) {
1508 |
1509 | // Prevent the 'I' cursor and extend the range-drag cursor.
1510 | document.body.style.cursor = getComputedStyle(event.target).cursor;
1511 |
1512 | // Mark the target with a dragging state.
1513 | if ( scope_Handles.length > 1 ) {
1514 | addClass(scope_Target, options.cssClasses.drag);
1515 | }
1516 |
1517 | var f = function(){
1518 | return false;
1519 | };
1520 |
1521 | document.body.noUiListener = f;
1522 |
1523 | // Prevent text selection when dragging the handles.
1524 | document.body.addEventListener('selectstart', f, false);
1525 | }
1526 |
1527 | data.handleNumbers.forEach(function(handleNumber){
1528 | fireEvent('start', handleNumber);
1529 | });
1530 | }
1531 |
1532 | // Move closest handle to tapped location.
1533 | function eventTap ( event ) {
1534 |
1535 | // The tap event shouldn't propagate up
1536 | event.stopPropagation();
1537 |
1538 | var proposal = calcPointToPercentage(event.calcPoint);
1539 | var handleNumber = getClosestHandle(proposal);
1540 |
1541 | // Tackle the case that all handles are 'disabled'.
1542 | if ( handleNumber === false ) {
1543 | return false;
1544 | }
1545 |
1546 | // Flag the slider as it is now in a transitional state.
1547 | // Transition takes a configurable amount of ms (default 300). Re-enable the slider after that.
1548 | if ( !options.events.snap ) {
1549 | addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration);
1550 | }
1551 |
1552 | setHandle(handleNumber, proposal, true, true);
1553 |
1554 | setZindex();
1555 |
1556 | fireEvent('slide', handleNumber, true);
1557 | fireEvent('set', handleNumber, true);
1558 | fireEvent('change', handleNumber, true);
1559 | fireEvent('update', handleNumber, true);
1560 |
1561 | if ( options.events.snap ) {
1562 | eventStart(event, { handleNumbers: [handleNumber] });
1563 | }
1564 | }
1565 |
1566 | // Fires a 'hover' event for a hovered mouse/pen position.
1567 | function eventHover ( event ) {
1568 |
1569 | var proposal = calcPointToPercentage(event.calcPoint);
1570 |
1571 | var to = scope_Spectrum.getStep(proposal);
1572 | var value = scope_Spectrum.fromStepping(to);
1573 |
1574 | Object.keys(scope_Events).forEach(function( targetEvent ) {
1575 | if ( 'hover' === targetEvent.split('.')[0] ) {
1576 | scope_Events[targetEvent].forEach(function( callback ) {
1577 | callback.call( scope_Self, value );
1578 | });
1579 | }
1580 | });
1581 | }
1582 |
1583 | // Attach events to several slider parts.
1584 | function bindSliderEvents ( behaviour ) {
1585 |
1586 | // Attach the standard drag event to the handles.
1587 | if ( !behaviour.fixed ) {
1588 |
1589 | scope_Handles.forEach(function( handle, index ){
1590 |
1591 | // These events are only bound to the visual handle
1592 | // element, not the 'real' origin element.
1593 | attachEvent ( actions.start, handle.children[0], eventStart, {
1594 | handleNumbers: [index]
1595 | });
1596 | });
1597 | }
1598 |
1599 | // Attach the tap event to the slider base.
1600 | if ( behaviour.tap ) {
1601 | attachEvent (actions.start, scope_Base, eventTap, {});
1602 | }
1603 |
1604 | // Fire hover events
1605 | if ( behaviour.hover ) {
1606 | attachEvent (actions.move, scope_Base, eventHover, { hover: true });
1607 | }
1608 |
1609 | // Make the range draggable.
1610 | if ( behaviour.drag ){
1611 |
1612 | scope_Connects.forEach(function( connect, index ){
1613 |
1614 | if ( connect === false || index === 0 || index === scope_Connects.length - 1 ) {
1615 | return;
1616 | }
1617 |
1618 | var handleBefore = scope_Handles[index - 1];
1619 | var handleAfter = scope_Handles[index];
1620 | var eventHolders = [connect];
1621 |
1622 | addClass(connect, options.cssClasses.draggable);
1623 |
1624 | // When the range is fixed, the entire range can
1625 | // be dragged by the handles. The handle in the first
1626 | // origin will propagate the start event upward,
1627 | // but it needs to be bound manually on the other.
1628 | if ( behaviour.fixed ) {
1629 | eventHolders.push(handleBefore.children[0]);
1630 | eventHolders.push(handleAfter.children[0]);
1631 | }
1632 |
1633 | eventHolders.forEach(function( eventHolder ) {
1634 | attachEvent ( actions.start, eventHolder, eventStart, {
1635 | handles: [handleBefore, handleAfter],
1636 | handleNumbers: [index - 1, index]
1637 | });
1638 | });
1639 | });
1640 | }
1641 | }
1642 |
1643 |
1644 | // Split out the handle positioning logic so the Move event can use it, too
1645 | function checkHandlePosition ( reference, handleNumber, to, lookBackward, lookForward ) {
1646 |
1647 | // For sliders with multiple handles, limit movement to the other handle.
1648 | // Apply the margin option by adding it to the handle positions.
1649 | if ( scope_Handles.length > 1 ) {
1650 |
1651 | if ( lookBackward && handleNumber > 0 ) {
1652 | to = Math.max(to, reference[handleNumber - 1] + options.margin);
1653 | }
1654 |
1655 | if ( lookForward && handleNumber < scope_Handles.length - 1 ) {
1656 | to = Math.min(to, reference[handleNumber + 1] - options.margin);
1657 | }
1658 | }
1659 |
1660 | // The limit option has the opposite effect, limiting handles to a
1661 | // maximum distance from another. Limit must be > 0, as otherwise
1662 | // handles would be unmoveable.
1663 | if ( scope_Handles.length > 1 && options.limit ) {
1664 |
1665 | if ( lookBackward && handleNumber > 0 ) {
1666 | to = Math.min(to, reference[handleNumber - 1] + options.limit);
1667 | }
1668 |
1669 | if ( lookForward && handleNumber < scope_Handles.length - 1 ) {
1670 | to = Math.max(to, reference[handleNumber + 1] - options.limit);
1671 | }
1672 | }
1673 |
1674 | to = scope_Spectrum.getStep(to);
1675 |
1676 | // Limit percentage to the 0 - 100 range
1677 | to = limit(to);
1678 |
1679 | // Return false if handle can't move
1680 | if ( to === reference[handleNumber] ) {
1681 | return false;
1682 | }
1683 |
1684 | return to;
1685 | }
1686 |
1687 | function toPct ( pct ) {
1688 | return pct + '%';
1689 | }
1690 |
1691 | // Updates scope_Locations and scope_Values, updates visual state
1692 | function updateHandlePosition ( handleNumber, to ) {
1693 |
1694 | // Update locations.
1695 | scope_Locations[handleNumber] = to;
1696 |
1697 | // Convert the value to the slider stepping/range.
1698 | scope_Values[handleNumber] = scope_Spectrum.fromStepping(to);
1699 |
1700 | // Called synchronously or on the next animationFrame
1701 | var stateUpdate = function() {
1702 | scope_Handles[handleNumber].style[options.style] = toPct(to);
1703 | updateConnect(handleNumber);
1704 | updateConnect(handleNumber + 1);
1705 | };
1706 |
1707 | // Set the handle to the new position.
1708 | // Use requestAnimationFrame for efficient painting.
1709 | // No significant effect in Chrome, Edge sees dramatic performace improvements.
1710 | // Option to disable is useful for unit tests, and single-step debugging.
1711 | if ( window.requestAnimationFrame && options.useRequestAnimationFrame ) {
1712 | window.requestAnimationFrame(stateUpdate);
1713 | } else {
1714 | stateUpdate();
1715 | }
1716 | }
1717 |
1718 | function setZindex ( ) {
1719 |
1720 | scope_HandleNumbers.forEach(function(handleNumber){
1721 | // Handles before the slider middle are stacked later = higher,
1722 | // Handles after the middle later is lower
1723 | // [[7] [8] .......... | .......... [5] [4]
1724 | var dir = (scope_Locations[handleNumber] > 50 ? -1 : 1);
1725 | var zIndex = 3 + (scope_Handles.length + (dir * handleNumber));
1726 | scope_Handles[handleNumber].childNodes[0].style.zIndex = zIndex;
1727 | });
1728 | }
1729 |
1730 | // Test suggested values and apply margin, step.
1731 | function setHandle ( handleNumber, to, lookBackward, lookForward ) {
1732 |
1733 | to = checkHandlePosition(scope_Locations, handleNumber, to, lookBackward, lookForward);
1734 |
1735 | if ( to === false ) {
1736 | return false;
1737 | }
1738 |
1739 | updateHandlePosition(handleNumber, to);
1740 |
1741 | return true;
1742 | }
1743 |
1744 | // Updates style attribute for connect nodes
1745 | function updateConnect ( index ) {
1746 |
1747 | // Skip connects set to false
1748 | if ( !scope_Connects[index] ) {
1749 | return;
1750 | }
1751 |
1752 | var l = 0;
1753 | var h = 100;
1754 |
1755 | if ( index !== 0 ) {
1756 | l = scope_Locations[index - 1];
1757 | }
1758 |
1759 | if ( index !== scope_Connects.length - 1 ) {
1760 | h = scope_Locations[index];
1761 | }
1762 |
1763 | scope_Connects[index].style[options.style] = toPct(l);
1764 | scope_Connects[index].style[options.styleOposite] = toPct(100 - h);
1765 | }
1766 |
1767 | // ...
1768 | function setValue ( to, handleNumber ) {
1769 |
1770 | // Setting with null indicates an 'ignore'.
1771 | // Inputting 'false' is invalid.
1772 | if ( to === null || to === false ) {
1773 | return;
1774 | }
1775 |
1776 | // If a formatted number was passed, attemt to decode it.
1777 | if ( typeof to === 'number' ) {
1778 | to = String(to);
1779 | }
1780 |
1781 | to = options.format.from(to);
1782 |
1783 | // Request an update for all links if the value was invalid.
1784 | // Do so too if setting the handle fails.
1785 | if ( to !== false && !isNaN(to) ) {
1786 | setHandle(handleNumber, scope_Spectrum.toStepping(to), false, false);
1787 | }
1788 | }
1789 |
1790 | // Set the slider value.
1791 | function valueSet ( input, fireSetEvent ) {
1792 |
1793 | var values = asArray(input);
1794 | var isInit = scope_Locations[0] === undefined;
1795 |
1796 | // Event fires by default
1797 | fireSetEvent = (fireSetEvent === undefined ? true : !!fireSetEvent);
1798 |
1799 | values.forEach(setValue);
1800 |
1801 | // Animation is optional.
1802 | // Make sure the initial values were set before using animated placement.
1803 | if ( options.animate && !isInit ) {
1804 | addClassFor(scope_Target, options.cssClasses.tap, options.animationDuration);
1805 | }
1806 |
1807 | // Now that all base values are set, apply constraints
1808 | scope_HandleNumbers.forEach(function(handleNumber){
1809 | setHandle(handleNumber, scope_Locations[handleNumber], true, false);
1810 | });
1811 |
1812 | setZindex();
1813 |
1814 | scope_HandleNumbers.forEach(function(handleNumber){
1815 |
1816 | fireEvent('update', handleNumber);
1817 |
1818 | // Fire the event only for handles that received a new value, as per #579
1819 | if ( values[handleNumber] !== null && fireSetEvent ) {
1820 | fireEvent('set', handleNumber);
1821 | }
1822 | });
1823 | }
1824 |
1825 | function valueReset ( fireSetEvent ) {
1826 | valueSet(options.start, fireSetEvent);
1827 | }
1828 |
1829 | // Get the slider value.
1830 | function valueGet ( ) {
1831 |
1832 | var values = scope_Values.map(options.format.to);
1833 |
1834 | // If only one handle is used, return a single value.
1835 | if ( values.length === 1 ){
1836 | return values[0];
1837 | }
1838 |
1839 | return values;
1840 | }
1841 |
1842 | // Removes classes from the root and empties it.
1843 | function destroy ( ) {
1844 |
1845 | for ( var key in options.cssClasses ) {
1846 | if ( !options.cssClasses.hasOwnProperty(key) ) { continue; }
1847 | removeClass(scope_Target, options.cssClasses[key]);
1848 | }
1849 |
1850 | while (scope_Target.firstChild) {
1851 | scope_Target.removeChild(scope_Target.firstChild);
1852 | }
1853 |
1854 | delete scope_Target.noUiSlider;
1855 | }
1856 |
1857 | // Get the current step size for the slider.
1858 | function getCurrentStep ( ) {
1859 |
1860 | // Check all locations, map them to their stepping point.
1861 | // Get the step point, then find it in the input list.
1862 | return scope_Locations.map(function( location, index ){
1863 |
1864 | var nearbySteps = scope_Spectrum.getNearbySteps( location );
1865 | var value = scope_Values[index];
1866 | var increment = nearbySteps.thisStep.step;
1867 | var decrement = null;
1868 |
1869 | // If the next value in this step moves into the next step,
1870 | // the increment is the start of the next step - the current value
1871 | if ( increment !== false ) {
1872 | if ( value + increment > nearbySteps.stepAfter.startValue ) {
1873 | increment = nearbySteps.stepAfter.startValue - value;
1874 | }
1875 | }
1876 |
1877 | // If the value is beyond the starting point
1878 | if ( value > nearbySteps.thisStep.startValue ) {
1879 | decrement = nearbySteps.thisStep.step;
1880 | }
1881 |
1882 | else if ( nearbySteps.stepBefore.step === false ) {
1883 | decrement = false;
1884 | }
1885 |
1886 | // If a handle is at the start of a step, it always steps back into the previous step first
1887 | else {
1888 | decrement = value - nearbySteps.stepBefore.highestStep;
1889 | }
1890 |
1891 | // Now, if at the slider edges, there is not in/decrement
1892 | if ( location === 100 ) {
1893 | increment = null;
1894 | }
1895 |
1896 | else if ( location === 0 ) {
1897 | decrement = null;
1898 | }
1899 |
1900 | // As per #391, the comparison for the decrement step can have some rounding issues.
1901 | var stepDecimals = scope_Spectrum.countStepDecimals();
1902 |
1903 | // Round per #391
1904 | if ( increment !== null && increment !== false ) {
1905 | increment = Number(increment.toFixed(stepDecimals));
1906 | }
1907 |
1908 | if ( decrement !== null && decrement !== false ) {
1909 | decrement = Number(decrement.toFixed(stepDecimals));
1910 | }
1911 |
1912 | return [decrement, increment];
1913 | });
1914 | }
1915 |
1916 | // Attach an event to this slider, possibly including a namespace
1917 | function bindEvent ( namespacedEvent, callback ) {
1918 | scope_Events[namespacedEvent] = scope_Events[namespacedEvent] || [];
1919 | scope_Events[namespacedEvent].push(callback);
1920 |
1921 | // If the event bound is 'update,' fire it immediately for all handles.
1922 | if ( namespacedEvent.split('.')[0] === 'update' ) {
1923 | scope_Handles.forEach(function(a, index){
1924 | fireEvent('update', index);
1925 | });
1926 | }
1927 | }
1928 |
1929 | // Undo attachment of event
1930 | function removeEvent ( namespacedEvent ) {
1931 |
1932 | var event = namespacedEvent && namespacedEvent.split('.')[0],
1933 | namespace = event && namespacedEvent.substring(event.length);
1934 |
1935 | Object.keys(scope_Events).forEach(function( bind ){
1936 |
1937 | var tEvent = bind.split('.')[0],
1938 | tNamespace = bind.substring(tEvent.length);
1939 |
1940 | if ( (!event || event === tEvent) && (!namespace || namespace === tNamespace) ) {
1941 | delete scope_Events[bind];
1942 | }
1943 | });
1944 | }
1945 |
1946 | // Updateable: margin, limit, step, range, animate, snap
1947 | function updateOptions ( optionsToUpdate, fireSetEvent ) {
1948 |
1949 | // Spectrum is created using the range, snap, direction and step options.
1950 | // 'snap' and 'step' can be updated, 'direction' cannot, due to event binding.
1951 | // If 'snap' and 'step' are not passed, they should remain unchanged.
1952 | var v = valueGet();
1953 |
1954 | var updateAble = ['margin', 'limit', 'range', 'animate', 'snap', 'step', 'format'];
1955 |
1956 | // Only change options that we're actually passed to update.
1957 | updateAble.forEach(function(name){
1958 | if ( optionsToUpdate[name] !== undefined ) {
1959 | originalOptions[name] = optionsToUpdate[name];
1960 | }
1961 | });
1962 |
1963 | var newOptions = testOptions(originalOptions);
1964 |
1965 | // Load new options into the slider state
1966 | updateAble.forEach(function(name){
1967 | if ( optionsToUpdate[name] !== undefined ) {
1968 | options[name] = newOptions[name];
1969 | }
1970 | });
1971 |
1972 | // Save current spectrum direction as testOptions in testRange call
1973 | // doesn't rely on current direction
1974 | newOptions.spectrum.direction = scope_Spectrum.direction;
1975 | scope_Spectrum = newOptions.spectrum;
1976 |
1977 | // Limit and margin depend on the spectrum but are stored outside of it. (#677)
1978 | options.margin = newOptions.margin;
1979 | options.limit = newOptions.limit;
1980 |
1981 | // Invalidate the current positioning so valueSet forces an update.
1982 | scope_Locations = [];
1983 | valueSet(optionsToUpdate.start || v, fireSetEvent);
1984 | }
1985 |
1986 | // Throw an error if the slider was already initialized.
1987 | if ( scope_Target.noUiSlider ) {
1988 | throw new Error('Slider was already initialized.');
1989 | }
1990 |
1991 | // Create the base element, initialise HTML and set classes.
1992 | // Add handles and connect elements.
1993 | addSlider(scope_Target);
1994 | addElements(options.connect, scope_Base);
1995 |
1996 | scope_Self = {
1997 | destroy: destroy,
1998 | steps: getCurrentStep,
1999 | on: bindEvent,
2000 | off: removeEvent,
2001 | get: valueGet,
2002 | set: valueSet,
2003 | reset: valueReset,
2004 | // Exposed for unit testing, don't use this in your application.
2005 | __moveHandles: function(a, b, c) { moveHandles(a, b, scope_Locations, c); },
2006 | options: originalOptions, // Issue #600, #678
2007 | updateOptions: updateOptions,
2008 | target: scope_Target, // Issue #597
2009 | pips: pips // Issue #594
2010 | };
2011 |
2012 | // Attach user events.
2013 | bindSliderEvents(options.events);
2014 |
2015 | // Use the public value method to set the start values.
2016 | valueSet(options.start);
2017 |
2018 | if ( options.pips ) {
2019 | pips(options.pips);
2020 | }
2021 |
2022 | if ( options.tooltips ) {
2023 | tooltips();
2024 | }
2025 |
2026 | return scope_Self;
2027 |
2028 | }
2029 |
2030 |
2031 | // Run the standard initializer
2032 | function initialize ( target, originalOptions ) {
2033 |
2034 | if ( !target.nodeName ) {
2035 | throw new Error('noUiSlider.create requires a single element.');
2036 | }
2037 |
2038 | // Test the options and create the slider environment;
2039 | var options = testOptions( originalOptions, target );
2040 | var api = closure( target, options, originalOptions );
2041 |
2042 | target.noUiSlider = api;
2043 |
2044 | return api;
2045 | }
2046 |
2047 | // Use an object instead of a function for future expansibility;
2048 | return {
2049 | create: initialize
2050 | };
2051 |
2052 | }));
--------------------------------------------------------------------------------
/src/assetbundles/tools/dist/js/tools.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Spicy Web
3 | * @author Josh Angell
4 | * @copyright Copyright (c) 2022, Spicy Web
5 | * @copyright Copyright (c) 2016, Supercool Ltd
6 | */
7 |
8 | (function($){
9 |
10 |
11 | if (typeof OddsAndEnds == 'undefined')
12 | {
13 | OddsAndEnds = {}
14 | }
15 |
16 |
17 | /**
18 | * Make all links in instructions open in a new window - useful for fields
19 | * where only markdown is supported
20 | */
21 | OddsAndEnds.TargetBlankInstructionLinks = Garnish.Base.extend(
22 | {
23 | init: function()
24 | {
25 | this.addListener(Garnish.$win, 'load resize', 'apply');
26 | },
27 |
28 | apply: function()
29 | {
30 | Garnish.$bod.find('.instructions a').each(function()
31 | {
32 | $(this).attr('target', '_blank');
33 | });
34 | }
35 | });
36 |
37 |
38 | /**
39 | * Search elements like Tags - forked from `Craft.TagSelectInput`
40 | */
41 | OddsAndEnds.ElementSearchInput = Craft.BaseElementSelectInput.extend(
42 | {
43 | searchTimeout: null,
44 | searchMenu: null,
45 | limit: null,
46 |
47 | $container: null,
48 | $elementsContainer: null,
49 | $elements: null,
50 | $addElementContainer: null,
51 | $addElementInput: null,
52 | $spinner: null,
53 |
54 | _ignoreBlur: false,
55 | _initialized: false,
56 |
57 | init: function(settings)
58 | {
59 | this.base($.extend({}, settings));
60 |
61 | this.$addElementContainer = this.$container.children('.add');
62 | this.$addElementInput = this.$addElementContainer.children('.text');
63 | this.$spinner = this.$addElementInput.next();
64 |
65 | // No reason for this to be sortable if we're only allowing 1 selection
66 | if (this.settings.limit == 1)
67 | {
68 | this.settings.sortable = false;
69 | }
70 |
71 | // Ping this so the input hides on load if it needs to
72 | this.updateAddElementsBtn();
73 |
74 | // Bind listeners
75 | this.addListener(this.$addElementInput, 'textchange', $.proxy(function()
76 | {
77 | if (this.searchTimeout)
78 | {
79 | clearTimeout(this.searchTimeout);
80 | }
81 |
82 | this.searchTimeout = setTimeout($.proxy(this, 'searchForElements'), 500);
83 | }, this));
84 |
85 | this.addListener(this.$addElementInput, 'keypress', function(ev)
86 | {
87 | if (ev.keyCode == Garnish.RETURN_KEY)
88 | {
89 | ev.preventDefault();
90 |
91 | if (this.searchMenu)
92 | {
93 | this.selectElement(this.searchMenu.$options[0]);
94 | }
95 | }
96 | });
97 |
98 | this.addListener(this.$addElementInput, 'focus', function()
99 | {
100 | if (this.searchMenu)
101 | {
102 | this.searchMenu.show();
103 | }
104 | });
105 |
106 | this.addListener(this.$addElementInput, 'blur', function()
107 | {
108 | if (this._ignoreBlur)
109 | {
110 | this._ignoreBlur = false;
111 | return;
112 | }
113 |
114 | setTimeout($.proxy(function()
115 | {
116 | if (this.searchMenu)
117 | {
118 | this.searchMenu.hide();
119 | }
120 | }, this), 1);
121 | });
122 |
123 | this._initialized = true;
124 | },
125 |
126 | getElements: function()
127 | {
128 | return this.$elementsContainer.find('.element');
129 | },
130 |
131 | // I’m hi-jacking these as they get fired when the limit is breached
132 | disableAddElementsBtn: function()
133 | {
134 | if (this.$addElementContainer && !this.$addElementContainer.hasClass('disabled'))
135 | {
136 | this.$addElementInput.prop('disabled', true);
137 | this.$addElementContainer
138 | .velocity('fadeOut', { duration: Craft.BaseElementSelectInput.ADD_FX_DURATION })
139 | .addClass('disabled');
140 | }
141 | },
142 | enableAddElementsBtn: function()
143 | {
144 | if (this.$addElementContainer && this.$addElementContainer.hasClass('disabled'))
145 | {
146 | this.$addElementInput.prop('disabled', false);
147 | this.$addElementContainer
148 | .velocity('fadeIn', { display: 'inline-block', duration: Craft.BaseElementSelectInput.REMOVE_FX_DURATION })
149 | .removeClass('disabled');
150 | this.$addElementInput.trigger('focus');
151 | }
152 | },
153 |
154 | searchForElements: function()
155 | {
156 | if (this.searchMenu)
157 | {
158 | this.killSearchMenu();
159 | }
160 |
161 | var val = this.$addElementInput.val();
162 |
163 | if (val)
164 | {
165 | this.$spinner.removeClass('hidden');
166 |
167 | var excludeIds = [];
168 | this.$elements.each(function()
169 | {
170 | var id = $(this).data('id');
171 | if (id)
172 | {
173 | excludeIds.push(id);
174 | }
175 | });
176 |
177 | if (this.settings.sourceElementId)
178 | {
179 | excludeIds.push(this.settings.sourceElementId);
180 | }
181 |
182 | var data = {
183 | search: this.$addElementInput.val(),
184 | sources: this.settings.sources,
185 | elementType: this.settings.elementType,
186 | excludeIds: excludeIds
187 | };
188 |
189 | Craft.postActionRequest('tools/tools/search-for-elements', data, $.proxy(function(response, textStatus)
190 | {
191 | this.$spinner.addClass('hidden');
192 |
193 | if (textStatus == 'success')
194 | {
195 | var $menu = $('').appendTo(Garnish.$bod);
196 |
197 | // Loop each source defined in our settings and see if we got anything back from it
198 | for (var i = 0; i < this.settings.sources.length; i++) {
199 | var sourceKey = this.settings.sources[i];
200 |
201 | if (response.elements.hasOwnProperty(sourceKey))
202 | {
203 | // Source name
204 | $('
'+response.elements[sourceKey][0].sourceName+'
').appendTo($menu);
205 | var $ul = $('
').appendTo($menu);
206 |
207 | // Elements in that source
208 | for (var n = 0; n < response.elements[sourceKey].length; n++)
209 | {
210 | var $li = $('').appendTo($ul),
211 | $a = $('').appendTo($li)
212 | .text(response.elements[sourceKey][n].title)
213 | .data('id', response.elements[sourceKey][n].id)
214 | .data('site-id', response.elements[sourceKey][n].siteId)
215 | .data('status', response.elements[sourceKey][n].status);
216 | $('').prependTo($a);
217 | }
218 | }
219 | }
220 |
221 | // This is a bit grubby, but we’re just checking the contents of the $menu
222 | // to see if there were no results returned
223 | if ( $menu.children().length == 0 )
224 | {
225 | $('
147 | * The return value is cast to an integer.
148 | */
149 | public function count()
150 | {
151 | $this->width ? 1 : 0;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/src/models/Settings.php:
--------------------------------------------------------------------------------
1 |
12 | * @author Supercool
13 | * @since 2.1.4
14 | */
15 | class Settings extends Model
16 | {
17 | public array $disableNormalFields = [];
18 | public array $disableCommerceFields = [];
19 | public array $disableWidgets = [];
20 |
21 | /**
22 | * @var int
23 | */
24 | public int $leftDefault = 0;
25 |
26 | /**
27 | * @var int
28 | */
29 | public int $rightDefault = 0;
30 |
31 | /**
32 | * @inheritdoc
33 | */
34 | protected function defineRules(): array
35 | {
36 | return [
37 | [['leftDefault', 'rightDefault'], 'required'],
38 | ];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/ancestors/settings.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 | {% set typeValue = 'ancestors'|t('tools') %}
3 |
4 | {{ targetLocaleField|raw }}
5 |
6 | {% if field['allowLimit'] %}
7 | {{ forms.textField({
8 | label: 'Min Relations'|t('app'),
9 | instructions: 'The minimum number of {type} that may be selected.'|t('app', {
10 | type: typeValue,
11 | }),
12 | id: 'min-relations',
13 | name: 'minRelations',
14 | value: field.minRelations,
15 | size: 2,
16 | errors: field.getErrors('minRelations'),
17 | }) }}
18 |
19 | {{ forms.textField({
20 | label: 'Max Relations'|t('app'),
21 | instructions: 'The maximum number of {type} that may be selected.'|t('app', {
22 | type: typeValue,
23 | }),
24 | id: 'max-relations',
25 | name: 'maxRelations',
26 | value: field.maxRelations,
27 | size: 2,
28 | errors: field.getErrors('maxRelations'),
29 | }) }}
30 | {% endif %}
31 |
32 | {{ forms.textField({
33 | label: 'Selection Label'|t('app'),
34 | instructions: 'Enter the text you want to appear on the {type} selection input.'|t('app', { type: typeValue }),
35 | id: 'selectionLabel',
36 | name: 'selectionLabel',
37 | value: field.selectionLabel,
38 | placeholder: field.defaultSelectionLabel(),
39 | errors: field.getErrors('selectionLabel')
40 | }) }}
41 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/authorinstructions/input.twig:
--------------------------------------------------------------------------------
1 |
2 | {{ authorInstructions | markdown }}
3 |
4 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/authorinstructions/settings.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 | {{
4 | forms.textareaField({
5 | label: 'Author Instructions' | t,
6 | instructions: 'Enter some instructions for the author here' | t,
7 | name: 'authorInstructions',
8 | id: 'authorInstructions',
9 | value: field['authorInstructions'],
10 | rows: 8,
11 | required: true,
12 | errors: field.getErrors('authorInstructions'),
13 | })
14 | }}
--------------------------------------------------------------------------------
/src/templates/_components/fields/categoriessearch/input.twig:
--------------------------------------------------------------------------------
1 | {% include 'tools/_includes/elementssearch' with {
2 | elementType: 'craft\\elements\\Category',
3 | } %}
4 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/disabledplaintext/settings.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 | {{ forms.textField({
4 | label: "Size"|t,
5 | instructions: "The size of the input."|t,
6 | id: 'size',
7 | name: 'size',
8 | value: field['size'],
9 | size: 3,
10 | errors: field.getErrors('size')
11 | }) }}
--------------------------------------------------------------------------------
/src/templates/_components/fields/entriessearch/input.twig:
--------------------------------------------------------------------------------
1 | {% include 'tools/_includes/elementssearch' with {
2 | elementType: 'craft\\elements\\Entry',
3 | } %}
4 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/grid/input.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 | {% do view.registerAssetBundle("spicyweb\\oddsandends\\assetbundles\\tools\\ToolsAsset") %}
4 |
5 | {# Available variables:
6 | value.totalColumns : {{ dump(value.totalColumns) }}
7 | value.maxColumnSpan (max width in columns, falsy values mean unlimited) : {{ dump(value.maxColumnSpan) }}
8 | value.minColumnSpan (min width in columns) : {{ dump(value.minColumnSpan) }}
9 | value.left : {{ dump(value.left) }}
10 | value.right : {{ dump(value.right) }} #}
11 |
12 | {# Default values to be used to see if field has a non-default value (and should be hidden)#}
13 | {# value.leftDefault (for comparison to value.left to see if field should be hidden) {{ dump(value.leftDefault) }}
14 | value.rightDefault (for comparison to value.right to see if field should be hidden) {{ dump(value.rightDefault) }}
15 |
16 | NB: Even if field is hidden it MUST still send left and right data to backend #}
17 |
18 | {# Text fields that allow for dummy saving of data.#}
19 | {# NOTE: This is the only data that needs to be sent to the backend (left and right values) #}
20 |
21 | {{ forms.hidden({
22 | label: 'left',
23 | name: name ~ '[left]',
24 | id: name ~ '-left',
25 | value: value.left
26 | }) }}
27 |
28 | {{ forms.hidden({
29 | label: 'right',
30 | name: name ~ '[right]',
31 | id: name ~ '-right',
32 | value: value.right
33 | }) }}
34 |
35 | {% if value.maxColumnSpan %}
36 | This block has a minimum width of {{value.minColumnSpan}} columns, and a maximum width of {{value.maxColumnSpan}} columns
37 | {% else %}
38 | This block has a minimum width of {{value.minColumnSpan}} columns
39 | {% endif %}
40 |
41 |
42 |