├── .editorconfig ├── .gitattributes ├── .scrutinizer.yml ├── .upgrade.yml ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _config.php ├── composer.json ├── css └── FrontEndGridField.css ├── images ├── arrows.png ├── btn-icon │ ├── cross.png │ ├── document--pencil.png │ ├── ellipsis.png │ └── magnifier.png └── icons │ ├── add.png │ ├── chain--minus.png │ ├── chain--plus.png │ ├── cross-circle.png │ ├── download-csv.png │ ├── filter-icons.png │ └── pagination-arrows.png ├── javascript ├── FrontEndGridField.js ├── GridField.js ├── boot.template.js └── externals │ ├── hafriedlander │ └── jquery-entwine │ │ └── jquery.entwine-dist.js │ └── silverstripe │ ├── lib.js │ └── ssui.core.js ├── src └── Forms │ └── GridField │ ├── GridField.php │ ├── GridFieldConfig_Base.php │ ├── GridFieldConfig_RecordEditor.php │ ├── GridFieldConfig_RecordViewer.php │ ├── GridFieldConfig_RelationEditor.php │ ├── GridFieldDetailForm.php │ └── GridFieldDetailForm_ItemRequest.php └── templates └── WebbuildersGroup └── FrontEndGridField └── Forms └── GridField ├── GridFieldDetailForm.ss └── GridField_deleted.ss /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.gitignore export-ignore 2 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | checks: 4 | php: 5 | verify_property_names: true 6 | verify_argument_usable_as_reference: true 7 | verify_access_scope_valid: true 8 | useless_calls: true 9 | use_statement_alias_conflict: true 10 | variable_existence: true 11 | unused_variables: true 12 | unused_properties: true 13 | unused_parameters: true 14 | unused_methods: true 15 | unreachable_code: true 16 | too_many_arguments: true 17 | sql_injection_vulnerabilities: true 18 | simplify_boolean_return: true 19 | side_effects_or_types: true 20 | security_vulnerabilities: true 21 | return_doc_comments: true 22 | return_doc_comment_if_not_inferrable: true 23 | require_scope_for_properties: true 24 | require_scope_for_methods: true 25 | require_php_tag_first: true 26 | psr2_switch_declaration: true 27 | psr2_class_declaration: true 28 | property_assignments: true 29 | prefer_while_loop_over_for_loop: true 30 | precedence_mistakes: true 31 | precedence_in_conditions: true 32 | phpunit_assertions: true 33 | php5_style_constructor: true 34 | parse_doc_comments: true 35 | parameter_non_unique: true 36 | parameter_doc_comments: true 37 | param_doc_comment_if_not_inferrable: true 38 | optional_parameters_at_the_end: true 39 | one_class_per_file: true 40 | no_unnecessary_if: true 41 | no_trailing_whitespace: true 42 | no_property_on_interface: true 43 | no_non_implemented_abstract_methods: true 44 | no_error_suppression: true 45 | no_duplicate_arguments: true 46 | no_commented_out_code: true 47 | newline_at_end_of_file: true 48 | missing_arguments: true 49 | method_calls_on_non_object: true 50 | instanceof_class_exists: true 51 | foreach_traversable: true 52 | fix_line_ending: true 53 | fix_doc_comments: true 54 | duplication: true 55 | deprecated_code_usage: true 56 | deadlock_detection_in_loops: true 57 | code_rating: true 58 | closure_use_not_conflicting: true 59 | catch_class_exists: true 60 | blank_line_after_namespace_declaration: false 61 | avoid_multiple_statements_on_same_line: true 62 | avoid_duplicate_types: true 63 | avoid_conflicting_incrementers: true 64 | avoid_closing_tag: true 65 | assignment_of_null_return: true 66 | argument_type_checks: true 67 | 68 | filter: 69 | paths: [code/*, tests/*] 70 | -------------------------------------------------------------------------------- /.upgrade.yml: -------------------------------------------------------------------------------- 1 | mappings: 2 | FrontEndGridField: WebbuildersGroup\FrontEndGridField\Forms\GridField\GridField 3 | FrontEndGridFieldConfig_Base: WebbuildersGroup\FrontEndGridField\Forms\GridField\GridFieldConfig_Base 4 | FrontEndGridFieldConfig_RecordViewer: WebbuildersGroup\FrontEndGridField\Forms\GridField\GridFieldConfig_RecordViewer 5 | FrontEndGridFieldConfig_RecordEditor: WebbuildersGroup\FrontEndGridField\Forms\GridField\GridFieldConfig_RecordEditor 6 | FrontEndGridFieldConfig_RelationEditor: WebbuildersGroup\FrontEndGridField\Forms\GridField\GridFieldConfig_RelationEditor 7 | FrontEndGridFieldDetailForm: WebbuildersGroup\FrontEndGridField\Forms\GridField\GridFieldDetailForm 8 | FrontEndGridFieldDetailForm_ItemRequest: WebbuildersGroup\FrontEndGridField\Forms\GridField\GridFieldDetailForm_ItemRequest 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [2.1.0](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/2.1.0) (2023-07-06) 4 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/2.0.6...2.1.0) 5 | 6 | ## [2.0.6](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/2.0.6) (2021-06-22) 7 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/2.0.5...2.0.6) 8 | 9 | ## [2.0.5](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/2.0.5) (2021-04-14) 10 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/2.0.4...2.0.5) 11 | 12 | ## [2.0.4](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/2.0.4) (2021-01-06) 13 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/2.0.3...2.0.4) 14 | 15 | ## [2.0.3](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/2.0.3) (2020-11-10) 16 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/2.0.2...2.0.3) 17 | 18 | ## [2.0.2](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/2.0.2) (2020-02-07) 19 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/2.0.1...2.0.2) 20 | 21 | ## [2.0.1](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/2.0.1) (2020-01-16) 22 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/2.0.0...2.0.1) 23 | 24 | ## [2.0.0](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/2.0.0) (2019-10-02) 25 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/1.0.0...2.0.0) 26 | 27 | ## [1.0.0](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/1.0.0) (2017-06-16) 28 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/0.3.0...1.0.0) 29 | 30 | ## [0.3.0](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/0.3.0) (2017-03-21) 31 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/0.2.0...0.3.0) 32 | 33 | **Closed issues:** 34 | 35 | - Build new Tag from current Master [\#9](https://github.com/webbuilders-group/silverstripe-frontendgridfield/issues/9) 36 | 37 | **Merged pull requests:** 38 | 39 | - fixes for SS 3.5 \(possibly 3.2+?\) [\#15](https://github.com/webbuilders-group/silverstripe-frontendgridfield/pull/15) ([xini](https://github.com/xini)) 40 | - Added standard Scrutinizer config [\#14](https://github.com/webbuilders-group/silverstripe-frontendgridfield/pull/14) ([helpfulrobot](https://github.com/helpfulrobot)) 41 | - Added standard .gitattributes file [\#13](https://github.com/webbuilders-group/silverstripe-frontendgridfield/pull/13) ([helpfulrobot](https://github.com/helpfulrobot)) 42 | - Added standard .editorconfig file [\#11](https://github.com/webbuilders-group/silverstripe-frontendgridfield/pull/11) ([helpfulrobot](https://github.com/helpfulrobot)) 43 | 44 | ## [0.2.0](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/0.2.0) (2014-12-13) 45 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/0.1.1...0.2.0) 46 | 47 | **Implemented enhancements:** 48 | 49 | - Unit Tests [\#1](https://github.com/webbuilders-group/silverstripe-frontendgridfield/issues/1) 50 | 51 | **Closed issues:** 52 | 53 | - Delete functions not working [\#4](https://github.com/webbuilders-group/silverstripe-frontendgridfield/issues/4) 54 | 55 | **Merged pull requests:** 56 | 57 | - Make sure jQuery is always included [\#7](https://github.com/webbuilders-group/silverstripe-frontendgridfield/pull/7) ([jedateach](https://github.com/jedateach)) 58 | - Allow customising FrontEndGridField css with themed css [\#6](https://github.com/webbuilders-group/silverstripe-frontendgridfield/pull/6) ([jedateach](https://github.com/jedateach)) 59 | - Added composer installer name [\#2](https://github.com/webbuilders-group/silverstripe-frontendgridfield/pull/2) ([jedateach](https://github.com/jedateach)) 60 | 61 | ## [0.1.1](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/0.1.1) (2014-01-03) 62 | [Full Changelog](https://github.com/webbuilders-group/silverstripe-frontendgridfield/compare/0.1...0.1.1) 63 | 64 | ## [0.1](https://github.com/webbuilders-group/silverstripe-frontendgridfield/tree/0.1) (2013-12-11) 65 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct). 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ================= 3 | ## Reporting an issue 4 | When you're reporting an issue please ensure you specify what version of SilverStripe you are using. Also be sure to include any JavaScript or PHP errors you receive, for PHP errors please ensure you include the full stack trace. Also please include your implementation code (where your setting up your grid field) as well as how you produced the issue. You may also be asked to provide some of the classes to aid in re-producing the issue. Stick with the issue, remember that you seen the issue not the maintainer of the module so it may take allot of questions to arrive at a fix or answer. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, The Web Builders Group Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of The Web Builders Group Inc nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Front-End GridField 2 | ================= 3 | Wraps gridfield adding support for using it on the front-end of a site. 4 | 5 | ## Maintainer Contact 6 | * Ed Chipman ([UndefinedOffset](https://github.com/UndefinedOffset)) 7 | 8 | ## Requirements 9 | * SilverStripe Framework 4.3+|5.0+ 10 | 11 | 12 | ## Installation 13 | * Download the module from here https://github.com/webbuilders-group/silverstripe-frontendgridfield/archive/master.zip 14 | * Extract the downloaded archive into your site root so that the destination folder is called frontendgridfield, opening the extracted folder should contain _config.php in the root along with other files/folders 15 | * Run dev/build?flush=all to regenerate the manifest 16 | 17 | 18 | ## Usage 19 | Instead of using the GridField class you need to use FrontEndGridField for use on the front-end, note it is not recommended to be used in the CMS. As well instead of using the GridFieldConfig extensions provided with SilverStripe use FrontEndGridFieldConfig_Base, FrontEndGridFieldConfig_RecordEditor, FrontEndGridFieldConfig_RecordViewer, or FrontEndGridFieldConfig_RelationEditor. If you are building your own GridField config ensure that you use FrontEndGridFieldDetailForm instead of GridFieldDetailForm. 20 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | div { 2 | margin-bottom: 36px; 3 | } 4 | 5 | form .ss-gridfield > div.addNewGridFieldButton { 6 | margin-bottom: 0; 7 | } 8 | 9 | form .ss-gridfield > div.addNewGridFieldButton .action { 10 | margin-bottom: 12px; 11 | } 12 | 13 | form .ss-gridfield > div.ss-gridfield-buttonrow-before { 14 | margin-bottom: 0; 15 | } 16 | 17 | form .ss-gridfield > div.ss-gridfield-buttonrow-before .action { 18 | display: inline-block; 19 | } 20 | 21 | form .ss-gridfield > div.ss-gridfield-buttonrow-before .action, 22 | form .ss-gridfield > div.ss-gridfield-buttonrow-before input { 23 | margin-bottom: 12px; 24 | } 25 | 26 | form .ss-gridfield > div.ss-gridfield-buttonrow-after { 27 | margin-bottom: 0; 28 | } 29 | 30 | form .ss-gridfield > div.ss-gridfield-buttonrow-after .action, 31 | form .ss-gridfield > div.ss-gridfield-buttonrow-after input { 32 | margin-top: 12px; 33 | } 34 | 35 | form .ss-gridfield[data-selectable] tr.ui-selected, 36 | form .ss-gridfield[data-selectable] tr.ui-selecting { 37 | background: #FFFAD6 !important; 38 | } 39 | 40 | form .ss-gridfield[data-selectable] td { 41 | cursor: pointer; 42 | } 43 | 44 | form .ss-gridfield span button.action_gridfield_relationfind { 45 | display: none; 46 | } 47 | 48 | form .ss-gridfield p button.action_export span.btn-icon-download-csv { 49 | height: 17px; 50 | } 51 | 52 | form .ss-gridfield .right { 53 | float: right; 54 | } 55 | 56 | form .ss-gridfield .right > * { 57 | float: right; 58 | margin-left: 8px; 59 | } 60 | 61 | form .ss-gridfield .pull-xs-right .pagination-records-number { 62 | font-size: 1.0em; 63 | padding: 6px 3px 6px 0; 64 | color: white; 65 | text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.2); 66 | font-weight: normal; 67 | } 68 | 69 | form .ss-gridfield .pull-xs-left { 70 | float: left; 71 | } 72 | 73 | form .ss-gridfield .pull-xs-left > * { 74 | margin-right: 8px; 75 | float: left; 76 | } 77 | 78 | form .ss-gridfield { 79 | overflow-x: hidden; 80 | } 81 | 82 | form .ss-gridfield .grid-levelup { 83 | text-indent: -9999em; 84 | margin-bottom: 6px; 85 | } 86 | 87 | form .ss-gridfield .grid-levelup a.list-parent-link { 88 | background: transparent url(../images/gridfield-level-up.png) no-repeat 0 0; 89 | display: block; 90 | } 91 | 92 | form .ss-gridfield .add-existing-autocompleter span { 93 | float: left; 94 | display: inline-block; 95 | vertical-align: top; 96 | } 97 | 98 | form .ss-gridfield .add-existing-autocompleter input.relation-search { 99 | width: 270px; 100 | height: 32px; 101 | margin-bottom: 12px; 102 | border-top-right-radius: 0; 103 | border-bottom-right-radius: 0; 104 | } 105 | 106 | form .ss-gridfield .add-existing-autocompleter button.action_gridfield_relationadd { 107 | height: 32px; 108 | margin-left: 0; 109 | border-top-left-radius: 0; 110 | border-bottom-left-radius: 0; 111 | border-left: none; 112 | } 113 | 114 | form .ss-gridfield .grid-csv-button, 115 | form .ss-gridfield .grid-print-button { 116 | margin-bottom: 0; 117 | font-size: 12px; 118 | display: -moz-inline-stack; 119 | display: inline-block; 120 | vertical-align: middle; 121 | } 122 | 123 | form table.grid-field__table { 124 | display: table; 125 | -moz-box-shadow: none; 126 | -webkit-box-shadow: none; 127 | box-shadow: none; 128 | padding: 0; 129 | border-collapse: separate; 130 | border-bottom: 0 none; 131 | width: 100%; 132 | } 133 | 134 | form table.grid-field__table thead { 135 | color: #323e46; 136 | background: transparent; 137 | } 138 | 139 | form table.grid-field__table thead tr.grid-field__filter-header.grid-field__search-holder--hidden { 140 | display: none; 141 | } 142 | 143 | form table.grid-field__table thead tr.grid-field__filter-header .fieldgroup { 144 | position: relative; 145 | 146 | display: flex; 147 | 148 | align-items: stretch; 149 | 150 | max-width: 512px; 151 | } 152 | 153 | form table.grid-field__table thead tr.grid-field__filter-header .fieldgroup .fieldgroup-field { 154 | padding: 0; 155 | 156 | flex: 1 1 auto; 157 | } 158 | 159 | form table.grid-field__table thead tr.grid-field__filter-header .fieldgroup .fieldgroup-field > .action { 160 | position: static; 161 | } 162 | 163 | form table.grid-field__table thead tr.grid-field__filter-header .fieldgroup .fieldgroup-field.last { 164 | position: absolute; 165 | right: 0; 166 | top: 0; 167 | 168 | width: auto; 169 | height: 100%; 170 | } 171 | 172 | form table.grid-field__table thead tr.grid-field__filter-header th:last-child .fieldgroup .fieldgroup-field.last { 173 | position: static; 174 | top: auto; 175 | right: auto; 176 | } 177 | 178 | form table.grid-field__table thead tr.grid-field__filter-header button.btn.ss-gridfield-button-reset { 179 | display: block; 180 | 181 | position: absolute; 182 | top: 50%; 183 | right: 0; 184 | 185 | padding: 0; 186 | 187 | opacity: 0.5; 188 | 189 | transform: translateY(-50%); 190 | 191 | border-radius: 0; 192 | } 193 | 194 | form table.grid-field__table thead tr.grid-field__filter-header button.btn.ss-gridfield-button-reset:hover { 195 | opacity: 1; 196 | } 197 | 198 | form table.grid-field__table thead tr.grid-field__filter-header button.btn.ss-gridfield-button-reset::before { 199 | background: url(./../images/btn-icon/cross.png) no-repeat center center; 200 | 201 | display: block; 202 | 203 | width: 100%; 204 | height: 100%; 205 | 206 | content: ""; 207 | } 208 | 209 | form table.grid-field__table thead tr:first-child th:first-child { 210 | -moz-border-radius-topleft: 5px; 211 | -webkit-border-top-left-radius: 5px; 212 | border-top-left-radius: 5px; 213 | } 214 | 215 | form table.grid-field__table thead tr:first-child th:last-child { 216 | -moz-border-radius-topright: 5px; 217 | -webkit-border-top-right-radius: 5px; 218 | border-top-right-radius: 5px; 219 | } 220 | 221 | form table.grid-field__table tbody { 222 | background: #FFF; 223 | } 224 | 225 | form table.grid-field__table tbody tr { 226 | cursor: pointer; 227 | } 228 | 229 | form table.grid-field__table tbody td { 230 | width: auto; 231 | max-width: 500px; 232 | word-wrap: break-word; 233 | } 234 | 235 | form table.grid-field__table tbody td.col-buttons { 236 | width: 1px; 237 | padding: 0 8px; 238 | text-align: right; 239 | white-space: nowrap; 240 | vertical-align: middle; 241 | } 242 | 243 | form table.grid-field__table tbody td.col-listChildrenLink { 244 | width: 16px; 245 | border-right: none; 246 | text-indent: -9999em; 247 | padding: 0; 248 | } 249 | 250 | form table.grid-field__table tbody td.col-listChildrenLink .list-children-link { 251 | background: transparent url(../images/sitetree_ss_default_icons.png) no-repeat 3px -4px; 252 | display: block; 253 | } 254 | 255 | form table.grid-field__table tbody td.col-getTreeTitle span.item { 256 | color: #0071c4; 257 | } 258 | 259 | form table.grid-field__table tbody td.col-getTreeTitle span.badge { 260 | clear: both; 261 | text-transform: uppercase; 262 | display: inline-block; 263 | padding: 0px 3px; 264 | font-size: 0.75em; 265 | line-height: 1em; 266 | margin-left: 10px; 267 | margin-right: 6px; 268 | margin-top: -1px; 269 | -moz-border-radius: 2px/2px; 270 | -webkit-border-radius: 2px 2px; 271 | border-radius: 2px/2px; 272 | } 273 | 274 | form table.grid-field__table tbody td.col-getTreeTitle span.badge.status-modified { 275 | color: #7E7470; 276 | border: 1px solid #C9B800; 277 | background-color: #FFF0BC; 278 | } 279 | 280 | form table.grid-field__table tbody td.col-getTreeTitle span.badge.status-addedtodraft { 281 | color: #7E7470; 282 | border: 1px solid #C9B800; 283 | background-color: #FFF0BC; 284 | } 285 | 286 | form table.grid-field__table tbody td.col-getTreeTitle span.badge.status-deletedonlive { 287 | color: #636363; 288 | border: 1px solid #E49393; 289 | background-color: #F2DADB; 290 | } 291 | 292 | form table.grid-field__table tbody td.col-getTreeTitle span.badge.status-removedfromdraft { 293 | color: #636363; 294 | border: 1px solid #E49393; 295 | background-color: #F2DADB; 296 | } 297 | 298 | form table.grid-field__table tbody td.col-getTreeTitle span.badge.status-workflow-approval { 299 | color: #56660C; 300 | border: 1px solid #7C8816; 301 | background-color: #DAE79A; 302 | } 303 | 304 | form table.grid-field__table tbody td button { 305 | border: none; 306 | background: none; 307 | padding: 1px 0; 308 | width: auto; 309 | text-shadow: none; 310 | } 311 | 312 | form table.grid-field__table tbody td button.ui-state-hover { 313 | background: none; 314 | -moz-box-shadow: none; 315 | -webkit-box-shadow: none; 316 | box-shadow: none; 317 | } 318 | 319 | form table.grid-field__table tbody td button.ui-state-active { 320 | border: none; 321 | -moz-box-shadow: none; 322 | -webkit-box-shadow: none; 323 | box-shadow: none; 324 | } 325 | 326 | form table.grid-field__table tbody td button.gridfield-button-delete { 327 | width: 20px; 328 | margin: 0; 329 | } 330 | 331 | form table.grid-field__table tbody td button.gridfield-button-delete span.btn-icon-decline { 332 | left: 2px; 333 | } 334 | 335 | form table.grid-field__table tbody td a.view-link, 336 | form table.grid-field__table tbody td a.edit-link { 337 | position: relative; 338 | } 339 | 340 | form table.grid-field__table tbody td a.view-link::before, 341 | form table.grid-field__table tbody td a.edit-link::before, 342 | form table.grid-field__table .action--delete::before { 343 | display: inline-block; 344 | 345 | width: 16px; 346 | height: 16px; 347 | 348 | content: ""; 349 | 350 | margin-right: 0.5em; 351 | 352 | vertical-align: middle; 353 | } 354 | 355 | form table.grid-field__table tbody td a.view-link::before { 356 | background: url(../images/btn-icon/magnifier.png) no-repeat 0 1px; 357 | } 358 | 359 | form table.grid-field__table tbody td a.edit-link::before { 360 | background: url(../images/btn-icon/document--pencil.png) no-repeat 2px 0px; 361 | } 362 | 363 | form table.grid-field__table .action--delete::before { 364 | background: url(./../images/icons/cross-circle.png) no-repeat; 365 | } 366 | 367 | form table.grid-field__table tfoot { 368 | color: #323e46; 369 | } 370 | 371 | form table.grid-field__table tfoot tr td { 372 | background: #b0bec7; 373 | padding: .7em; 374 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 375 | } 376 | 377 | form table.grid-field__table tr.grid-field__title-row th { 378 | position: relative; 379 | background: #98aab6; 380 | border-bottom: 1px solid #899eab; 381 | padding: 5px; 382 | min-height: 40px; 383 | background-image: 384 | url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzk4YWFiNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); 385 | background-size: 100%; 386 | background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), 387 | color-stop(100%, #98aab6)); 388 | background-image: -moz-linear-gradient(#b0bec7, #98aab6); 389 | background-image: -webkit-linear-gradient(#b0bec7, #98aab6); 390 | background-image: linear-gradient(#b0bec7, #98aab6); 391 | text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.4); 392 | } 393 | 394 | form table.grid-field__table tr.grid-field__title-row th h2 { 395 | padding: 0px; 396 | font-size: 16.8px; 397 | color: #fff; 398 | margin: 1px 8px 0; 399 | display: inline-block; 400 | float: left; 401 | } 402 | 403 | form table.grid-field__table tr.sortable-header { 404 | background: #dbe3e8; 405 | } 406 | 407 | form table.grid-field__table tr.sortable-header th { 408 | padding: 0; 409 | font-weight: normal; 410 | } 411 | 412 | form table.grid-field__table tr.sortable-header th .btn { 413 | font-weight: normal; 414 | } 415 | 416 | form table.grid-field__table tr:hover { 417 | background: #FFFAD6; 418 | } 419 | 420 | form table.grid-field__table tr:first-child { 421 | background: transparent; 422 | } 423 | 424 | form table.grid-field__table tbody tr:first-child:hover { 425 | background: #FFFAD6; 426 | } 427 | 428 | form table.grid-field__table tr.ss-gridfield-even { 429 | background: #F0F4F7; 430 | } 431 | 432 | form table.grid-field__table tr.ss-gridfield-even.ss-gridfield-last { 433 | border-bottom: none; 434 | } 435 | 436 | form table.grid-field__table tr.ss-gridfield-even:hover { 437 | background: #FFFAD6; 438 | } 439 | 440 | form table.grid-field__table tr.even { 441 | background: #F0F4F7; 442 | } 443 | 444 | form table.grid-field__table tr.even:hover { 445 | background: #FFFAD6; 446 | } 447 | 448 | form table.grid-field__table tr th { 449 | font-weight: bold; 450 | font-size: 12px; 451 | color: #FFF; 452 | padding: 5px; 453 | border-right: 1px solid rgba(0, 0, 0, 0.1); 454 | text-align: left; 455 | } 456 | 457 | form table.grid-field__table tr th div.fieldgroup, 458 | form table.grid-field__table tr th div.fieldgroup-field { 459 | width: 100%; 460 | position: relative; 461 | } 462 | 463 | form table.grid-field__table tr th div.fieldgroup { 464 | padding-right: 0; 465 | } 466 | 467 | form table.grid-field__table tr th div.fieldgroup.filter-buttons { 468 | min-width: 49px; 469 | box-shadow: none; 470 | border: none; 471 | } 472 | 473 | form table.grid-field__table tr th div.fieldgroup.filter-buttons div { 474 | width: auto; 475 | display: inline; 476 | } 477 | 478 | form table.grid-field__table tr th.main { 479 | white-space: nowrap; 480 | border-top: 1px solid #a4b4bf; 481 | border-left: 1px solid #a4b4bf; 482 | color: #fff; 483 | background: #98aab6; 484 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 485 | } 486 | 487 | form table.grid-field__table tr th.main span { 488 | text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.2); 489 | padding-left: 8px; 490 | padding-right: 8px; 491 | overflow: hidden; 492 | white-space: nowrap; 493 | text-overflow: ellipsis; 494 | -o-text-overflow: ellipsis; 495 | margin-right: 8px; 496 | } 497 | 498 | form table.grid-field__table tr th.main.col-listChildrenLink { 499 | border-right: none; 500 | } 501 | 502 | form table.grid-field__table tr th.extra, 503 | form table.grid-field__table tr th.action { 504 | padding: 0; 505 | cursor: default; 506 | } 507 | 508 | form table.grid-field__table tr th.extra { 509 | position: relative; 510 | background: #637276; 511 | background: rgba(0, 0, 0, 0.7); 512 | padding: 5px; 513 | border-top: rgba(0, 0, 0, 0.2); 514 | } 515 | 516 | form table.grid-field__table tr th.extra input { 517 | height: 28px; 518 | } 519 | 520 | form table.grid-field__table tr th.extra button.btn { 521 | padding: .3em; 522 | line-height: 1; 523 | -moz-box-shadow: none; 524 | -webkit-box-shadow: none; 525 | box-shadow: none; 526 | position: relative; 527 | border-bottom-width: 0; 528 | -moz-border-radius: 2px/2px; 529 | -webkit-border-radius: 2px 2px; 530 | border-radius: 2px/2px; 531 | } 532 | 533 | form table.grid-field__table tr th.extra select { 534 | margin: 0; 535 | } 536 | 537 | form table.grid-field__table tr th button.action_gridfield_relationadd:hover { 538 | color: #444 !important; 539 | /* Not sure why IE think it needs this */ 540 | } 541 | 542 | form table.grid-field__table tr th button:hover { 543 | color: #ccc !important; 544 | /* Not sure why IE think it needs this */ 545 | } 546 | 547 | form table.grid-field__table tr th button.grid-field__sort:hover { 548 | color: #fff !important; 549 | -moz-box-shadow: none; 550 | -webkit-box-shadow: none; 551 | box-shadow: none; 552 | } 553 | 554 | form table.grid-field__table tr th button.grid-field__sort { 555 | background: transparent url(../images/arrows.png) no-repeat right 6px; 556 | border: none; 557 | width: 100%; 558 | text-align: left; 559 | padding: 2px 8px 2px 0; 560 | text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.2); 561 | color: #fff; 562 | -moz-border-radius: 0; 563 | -webkit-border-radius: 0; 564 | border-radius: 0; 565 | } 566 | 567 | form table.grid-field__table tr th button.grid-field__sort:hover { 568 | background-position: right -34px; 569 | } 570 | 571 | form table.grid-field__table tr th button.grid-field__sort.ss-gridfield-sorted-desc, 572 | form table.grid-field__table tr th button.grid-field__sort.grid-field__sorted-desc { 573 | background-position: right -72px; 574 | } 575 | 576 | form table.grid-field__table tr th button.grid-field__sort.ss-gridfield-sorted-asc, 577 | form table.grid-field__table tr th button.grid-field__sort.grid-field__sorted-asc { 578 | background-position: right -116px; 579 | } 580 | 581 | form table.grid-field__table tr th button.btn.font-icon-search { 582 | background-color: #55a4d2; 583 | border: none; 584 | display: block; 585 | text-indent: -9999em; 586 | width: 30px; 587 | height: 25px; 588 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 589 | url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzMzOGRjMSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzI4NzA5OSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); 590 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 591 | -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #338dc1), 592 | color-stop(100%, #287099)); 593 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 594 | -moz-linear-gradient(#338dc1, #287099); 595 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 596 | -webkit-linear-gradient(#338dc1, #287099); 597 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 598 | linear-gradient(#338dc1, #287099); 599 | width: 26px; 600 | border-top: 1px solid #4199cd; 601 | } 602 | 603 | form table.grid-field__table tr th button.btn.font-icon-search.hover-alike:active, 604 | form table.grid-field__table tr th button.btn.font-icon-search:active, 605 | form table.grid-field__table tr th button.btn.font-icon-search.hover-alike, 606 | form table.grid-field__table tr th button.btn.font-icon-search:hover { 607 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 608 | url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzU1YTRkMiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzMzOGRjMSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); 609 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 610 | -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #55a4d2), 611 | color-stop(100%, #338dc1)); 612 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 613 | -moz-linear-gradient(#55a4d2, #338dc1); 614 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 615 | -webkit-linear-gradient(#55a4d2, #338dc1); 616 | background: url(../images/icons/filter-icons.png) no-repeat -15px 4px, 617 | linear-gradient(#55a4d2, #338dc1); 618 | } 619 | 620 | form table.grid-field__table tr th button.btn.font-icon-search.grid-field__filter-open { 621 | margin-left: 12px; 622 | border: none; 623 | background: url(../images/icons/filter-icons.png) no-repeat -17px 6px; 624 | padding-right: 46px; 625 | margin: 0 6px; 626 | } 627 | 628 | form table.grid-field__table tr th button.btn.font-icon-search.grid-field__filter-open span { 629 | opacity: 0.4; 630 | position: absolute; 631 | width: 10px; 632 | left: 30px; 633 | top: 40%; 634 | background: url(../admin/images/btn_arrow_down_grey.png) no-repeat 0px 0px; 635 | } 636 | 637 | form table.grid-field__table tr th button.btn.font-icon-search.grid-field__filter-open:hover { 638 | background: url(../images/icons/filter-icons.png) no-repeat -17px -38px; 639 | -moz-box-shadow: none; 640 | -webkit-box-shadow: none; 641 | box-shadow: none; 642 | } 643 | 644 | form table.grid-field__table tr th button.btn.font-icon-search.grid-field__filter-open:hover span { 645 | opacity: 0.9; 646 | } 647 | 648 | form table.grid-field__table tr th button.btn.ss-gridfield-button-close { 649 | background: url(../images/icons/filter-icons.png) no-repeat 8px -17px; 650 | border: none; 651 | display: block; 652 | text-indent: -9999em; 653 | width: 30px; 654 | height: 25px; 655 | width: 25px; 656 | opacity: 0.8; 657 | margin-right: -5px; 658 | } 659 | 660 | form table.grid-field__table tr th button.btn.ss-gridfield-button-close.hover-alike:active, 661 | form table.grid-field__table tr th button.btn.ss-gridfield-button-close:active, 662 | form table.grid-field__table tr th button.btn.ss-gridfield-button-close.hover-alike, 663 | form table.grid-field__table tr th button.btn.ss-gridfield-button-close:hover { 664 | opacity: 1; 665 | background: url(../images/icons/filter-icons.png) no-repeat 8px -17px, 666 | url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIwLjEiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmZmZmYiIHN0b3Atb3BhY2l0eT0iMC4xIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g'); 667 | background: url(../images/icons/filter-icons.png) no-repeat 8px -17px, 668 | -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(255, 255, 669 | 255, 0.1)), color-stop(100%, rgba(255, 255, 255, 0.1))); 670 | background: url(../images/icons/filter-icons.png) no-repeat 8px -17px, 671 | -moz-linear-gradient(rgba(255, 255, 255, 0.1), 672 | rgba(255, 255, 255, 0.1)); 673 | background: url(../images/icons/filter-icons.png) no-repeat 8px -17px, 674 | -webkit-linear-gradient(rgba(255, 255, 255, 0.1), 675 | rgba(255, 255, 255, 0.1)); 676 | background: url(../images/icons/filter-icons.png) no-repeat 8px -17px, 677 | linear-gradient(rgba(255, 255, 255, 0.1), 678 | rgba(255, 255, 255, 0.1)); 679 | } 680 | 681 | form table.grid-field__table tr th button.btn.ss-gridfield-button-reset { 682 | border: none; 683 | display: block; 684 | text-indent: -9999em; 685 | width: 30px; 686 | height: 25px; 687 | position: absolute; 688 | top: -21px; 689 | right: -1px; 690 | width: 20px; 691 | height: 20px; 692 | display: none; 693 | } 694 | 695 | form table.grid-field__table tr th button.btn.ss-gridfield-button-reset.filtered { 696 | display: block; 697 | background: url(../admin/images/btn-icon/cross.png) no-repeat 0px 0px; 698 | opacity: 0.5; 699 | } 700 | 701 | form table.grid-field__table tr th button.btn.ss-gridfield-button-reset.filtered:hover { 702 | opacity: 0.8; 703 | } 704 | 705 | form table.grid-field__table tr th button.btn.ss-gridfield-button-reset.filtered:active { 706 | opacity: 1; 707 | } 708 | 709 | form table.grid-field__table tr th input.grid-field__sort { 710 | height: 25px; 711 | padding: 4px; 712 | border: 1px solid #313232; 713 | } 714 | 715 | form table.grid-field__table tr th input.grid-field__sort::-webkit-input-placeholder { 716 | font-style: italic; 717 | color: #ced5d7; 718 | } 719 | 720 | form table.grid-field__table tr th input.grid-field__sort:-moz-placeholder { 721 | font-style: italic; 722 | color: #ced5d7; 723 | } 724 | 725 | form table.grid-field__table tr th input.grid-field__sort:-ms-input-placeholder { 726 | font-style: italic; 727 | color: #ced5d7; 728 | } 729 | 730 | form table.grid-field__table tr th input.grid-field__sort:placeholder { 731 | font-style: italic; 732 | color: #ced5d7; 733 | } 734 | 735 | form table.grid-field__table tr th input.grid-field__sort:focus { 736 | -moz-box-shadow: none; 737 | -webkit-box-shadow: none; 738 | box-shadow: none; 739 | } 740 | 741 | form table.grid-field__table tr th span.non-sortable { 742 | display: block; 743 | padding: 6px 8px; 744 | } 745 | 746 | form table.grid-field__table tr td { 747 | border-right: 1px solid rgba(0, 0, 0, 0.1); 748 | padding: 8px 8px; 749 | color: #666; 750 | } 751 | 752 | form table.grid-field__table tr td.bottom-all { 753 | -moz-border-radius-bottomleft: 5px; 754 | -webkit-border-bottom-left-radius: 5px; 755 | border-bottom-left-radius: 5px; 756 | -moz-border-radius-bottomright: 5px; 757 | -webkit-border-bottom-right-radius: 5px; 758 | border-bottom-right-radius: 5px; 759 | background-image: 760 | url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzk4YWFiNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); 761 | background-size: 100%; 762 | background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), 763 | color-stop(100%, #98aab6)); 764 | background-image: -moz-linear-gradient(#b0bec7, #98aab6); 765 | background-image: -webkit-linear-gradient(#b0bec7, #98aab6); 766 | background-image: linear-gradient(#b0bec7, #98aab6); 767 | padding: 4px 12px; 768 | } 769 | 770 | form table.grid-field__table tr td.bottom-all .datagrid-footer-message { 771 | text-align: center; 772 | padding-top: 6px; 773 | color: white; 774 | } 775 | 776 | form table.grid-field__table tr td.bottom-all .datagrid-pagination { 777 | padding-top: 1px; 778 | position: absolute; 779 | left: 50%; 780 | margin-left: -116px; 781 | z-index: 5; 782 | } 783 | 784 | form table.grid-field__table tr td.bottom-all .datagrid-pagination .pagination-page-number { 785 | color: white; 786 | text-align: center; 787 | text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.2); 788 | } 789 | 790 | form table.grid-field__table tr td.bottom-all .datagrid-pagination .pagination-page-number input { 791 | width: 35px; 792 | height: 18px; 793 | margin-bottom: -6px; 794 | padding: 0px; 795 | border: 1px solid #899eab; 796 | border-bottom: 1px solid #a7b7c1; 797 | } 798 | 799 | form table.grid-field__table tr td.bottom-all .datagrid-pagination button { 800 | -moz-box-shadow: none; 801 | -webkit-box-shadow: none; 802 | box-shadow: none; 803 | border: none; 804 | width: 10px; 805 | margin: 0 10px; 806 | display: inline; 807 | float: none; 808 | } 809 | 810 | form table.grid-field__table tr td.bottom-all .datagrid-pagination button span { 811 | text-indent: -9999em; 812 | } 813 | 814 | form table.grid-field__table tr td.bottom-all .datagrid-pagination button.ss-gridfield-previouspage { 815 | background: url(../images/icons/pagination-arrows.png) no-repeat -23px 8px; 816 | } 817 | 818 | form table.grid-field__table tr td.bottom-all .datagrid-pagination button.ss-gridfield-nextpage { 819 | background: url(../images/icons/pagination-arrows.png) no-repeat -47px 8px; 820 | } 821 | 822 | form table.grid-field__table tr td.bottom-all .datagrid-pagination button.ss-gridfield-firstpage { 823 | background: url(../images/icons/pagination-arrows.png) no-repeat 0px 8px; 824 | } 825 | 826 | form table.grid-field__table tr td.bottom-all .datagrid-pagination button.ss-gridfield-lastpage { 827 | background: url(../images/icons/pagination-arrows.png) no-repeat -73px 8px; 828 | } 829 | 830 | form table.grid-field__table tr td.bottom-all .datagrid-pagination button.ssui-button-disabled { 831 | z-index: -1; 832 | } 833 | 834 | form table.grid-field__table tr td.bottom-all .pagination-records-number { 835 | float: right; 836 | padding: 6px 0; 837 | color: white; 838 | text-shadow: 0px -1px 0 rgba(0, 0, 0, 0.2); 839 | } 840 | 841 | form table.grid-field__table tr.last td { 842 | border-bottom: 0 none; 843 | } 844 | 845 | form table.grid-field__table td:first-child { 846 | border-left: 1px solid rgba(0, 0, 0, 0.1); 847 | } 848 | 849 | form table.grid-field__table td:last-child { 850 | border-right: 1px solid rgba(0, 0, 0, 0.1); 851 | } 852 | 853 | form table.grid-field__table td.col-StripThumbnail { 854 | padding: 2px 4px; 855 | width: 32px; 856 | height: 32px; 857 | } 858 | 859 | form table.grid-field__table td.col-StripThumbnail img { 860 | width: 32px; 861 | height: 32px; 862 | display: block; 863 | } 864 | 865 | form .ss-gridfield .btn, form .ss-gridfield .action { 866 | position: relative; 867 | } 868 | 869 | form .ss-gridfield th.extra .action { 870 | border: 0; 871 | } 872 | 873 | form .ss-gridfield .ui-button-text { 874 | display: block; 875 | 876 | padding: .4em 1em .4em 2.1em; 877 | 878 | line-height: 1.4; 879 | } 880 | 881 | form .ss-gridfield th.extra .ui-button-text { 882 | display: inline-block; 883 | 884 | padding: 0; 885 | 886 | line-height: normal; 887 | } 888 | 889 | form .ss-gridfield .btn.font-icon-plus-circled::before, 890 | form .ss-gridfield .action.action_gridfield_relationadd::before, 891 | form .ss-gridfield .font-icon-link-broken::before, 892 | form .ss-gridfield-buttonrow .action_export::before { 893 | display: inline-block; 894 | 895 | position: static; 896 | top: auto; 897 | left: auto; 898 | 899 | content: ""; 900 | 901 | width: 16px; 902 | height: 16px; 903 | 904 | vertical-align: middle; 905 | } 906 | 907 | form .ss-gridfield .btn.font-icon-plus-circled::before { 908 | background: url(./../images/icons/add.png) no-repeat; 909 | } 910 | 911 | form .ss-gridfield .action.action_gridfield_relationadd::before { 912 | background: url(./../images/icons/chain--plus.png) no-repeat; 913 | } 914 | 915 | form .ss-gridfield .font-icon-link-broken::before { 916 | background: url(./../images/icons/chain--minus.png) no-repeat; 917 | } 918 | 919 | form .ss-gridfield-buttonrow .action_export::before { 920 | background: url(./../images/icons/download-csv.png) no-repeat; 921 | } 922 | 923 | form table.grid-field__table tr th input.grid-field__sort-field { 924 | width: 100%; 925 | height: auto; 926 | margin: 0; 927 | outline: none; 928 | border-radius: 4px; 929 | line-height: 16px; 930 | box-sizing: border-box; 931 | } 932 | 933 | form table.grid-field__table thead tr.grid-field__filter-header .fieldgroup .fieldgroup-field { 934 | float: left; 935 | } 936 | 937 | form table.grid-field__table td.ss-gridfield-item.loading { 938 | position: relative; 939 | 940 | height: 100px; 941 | } 942 | 943 | form .grid-field .pull-xs-right > * { 944 | float: right; 945 | margin-left: .6154rem; 946 | } 947 | 948 | form .grid-field .dropdown, form .grid-field .dropleft, 949 | form .grid-field .dropright, form .grid-field .dropup { 950 | position: relative; 951 | } 952 | 953 | form .grid-field .dropdown-menu { 954 | position: absolute; 955 | top: 100%; 956 | left: 0; 957 | z-index: 1000; 958 | display: none; 959 | float: left; 960 | min-width: 10rem; 961 | padding: .5rem 0; 962 | margin: .125rem 0 0; 963 | font-size: 1rem; 964 | color: #43536d; 965 | text-align: left; 966 | list-style: none; 967 | background-color: #fff; 968 | -webkit-background-clip: padding-box; 969 | background-clip: padding-box; 970 | border: 1px solid rgba(0,0,0,.15); 971 | border-radius: .23rem; 972 | -webkit-box-shadow: 0 .5rem 1rem rgba(0,0,0,.175); 973 | box-shadow: 0 .5rem 1rem rgba(0,0,0,.175); 974 | 975 | text-align: left; 976 | } 977 | 978 | form .grid-field .dropdown-menu.show { 979 | display: block; 980 | } 981 | 982 | form .grid-field .dropdown-menu-right { 983 | right: 0; 984 | left: auto; 985 | } 986 | 987 | form .grid-field .dropdown-menu[x-placement^="bottom"], form .grid-field .dropdown-menu[x-placement^="left"], 988 | form .grid-field .dropdown-menu[x-placement^="right"], form .grid-field .dropdown-menu[x-placement^="top"] { 989 | right: auto; 990 | bottom: auto; 991 | } 992 | 993 | form .grid-field .action-menu__dropdown .dropdown-item { 994 | cursor: pointer; 995 | } 996 | 997 | form .grid-field .dropdown-item { 998 | display: block; 999 | width: 100%; 1000 | margin: 0; 1001 | padding: .45rem 1.3rem; 1002 | clear: both; 1003 | font-weight: 400; 1004 | color: #43536d; 1005 | text-align: left !important; 1006 | white-space: nowrap; 1007 | background-color: transparent; 1008 | border: 0; 1009 | box-sizing: border-box; 1010 | } 1011 | 1012 | form .grid-field .dropdown-item:focus, form .grid-field .dropdown-item:hover { 1013 | color: #43536d; 1014 | text-decoration: none; 1015 | background-color: #eef0f4; 1016 | } 1017 | 1018 | form .grid-field .gridfield-actionmenu__container .action-menu__dropdown { 1019 | left: auto !important; 1020 | right: auto; 1021 | } 1022 | 1023 | form .grid-field .grid-field__col-compact > * { 1024 | vertical-align: middle; 1025 | margin-right: 6px; 1026 | } 1027 | 1028 | form .grid-field .gridfield-actionmenu__container { 1029 | display: -webkit-inline-box; 1030 | display: -webkit-inline-flex; 1031 | display: inline-flex; 1032 | margin: -.5em 0; 1033 | } 1034 | 1035 | form .grid-field .grid-field__col-compact > :last-child { 1036 | margin-right: 0; 1037 | } 1038 | 1039 | form .grid-field .action-menu__toggle { 1040 | width: 1.4em; 1041 | } 1042 | 1043 | form .grid-field .action-menu__toggle::before { 1044 | background: url(../images/btn-icon/ellipsis.png) no-repeat center center; 1045 | 1046 | display: block; 1047 | 1048 | width: 16px; 1049 | height: 16px; 1050 | 1051 | content: ""; 1052 | 1053 | margin: 0 auto; 1054 | } 1055 | 1056 | form .grid-field .action-menu__toggle .sr-only { 1057 | display: none; 1058 | } 1059 | 1060 | form .grid-field span button.action.action_gridfield_relationfind { 1061 | display: none; 1062 | } 1063 | 1064 | form .grid-field .cms-content-loading-spinner { 1065 | position: absolute; 1066 | top: 0; 1067 | left: 0; 1068 | width: 100%; 1069 | height: 100%; 1070 | z-index: 9999; 1071 | display: -webkit-box; 1072 | display: -webkit-flex; 1073 | display: flex; 1074 | -webkit-box-align: center; 1075 | -webkit-align-items: center; 1076 | align-items: center; 1077 | -webkit-box-pack: center; 1078 | -webkit-justify-content: center; 1079 | justify-content: center 1080 | } 1081 | 1082 | form .grid-field .cms-content-loading-spinner .spinner { 1083 | width: 3rem; 1084 | height: 3rem; 1085 | padding: .5rem; 1086 | background-color: #fff; 1087 | border-radius: .192rem 1088 | } 1089 | 1090 | form .grid-field .cms-content-loading-spinner .spinner__animation { 1091 | width: 100%; 1092 | height: 100% 1093 | } 1094 | 1095 | form .grid-field .cms-content-loading-spinner .spinner__animation__empty { 1096 | fill: #8f9fba 1097 | } 1098 | 1099 | form .grid-field .cms-content-loading-spinner .spinner__animation__fill { 1100 | fill: none; 1101 | stroke: #005a93; 1102 | stroke-width: 5; 1103 | stroke-dasharray: 70; 1104 | -webkit-transform: translateZ(0); 1105 | transform: translateZ(0); 1106 | -webkit-animation: frontgridfield__spinner__animation__keyframes 1.5s infinite cubic-bezier(.445, .05, .55, .95) forwards; 1107 | -o-animation: frontgridfield__spinner__animation__keyframes 1.5s infinite cubic-bezier(.445, .05, .55, .95) forwards; 1108 | animation: frontgridfield__spinner__animation__keyframes 1.5s infinite cubic-bezier(.445, .05, .55, .95) forwards 1109 | } 1110 | 1111 | @media (-ms-high-contrast:none) { 1112 | form .grid-field .cms-content-loading-spinner .spinner__animation__fill { 1113 | -webkit-animation: frontgridfield__spinner__animation__keyframes_ie .5s alternate infinite cubic-bezier(.445, .05, .55, .95) forwards; 1114 | -o-animation: frontgridfield__spinner__animation__keyframes_ie .5s alternate infinite cubic-bezier(.445, .05, .55, .95) forwards; 1115 | animation: frontgridfield__spinner__animation__keyframes_ie .5s alternate infinite cubic-bezier(.445, .05, .55, .95) forwards 1116 | } 1117 | } 1118 | 1119 | @-webkit-keyframes frontgridfield__spinner__animation__keyframes { 1120 | 0% { 1121 | stroke-dashoffset: 140 1122 | } 1123 | 1124 | to { 1125 | stroke-dashoffset: 0 1126 | } 1127 | } 1128 | 1129 | @-o-keyframes frontgridfield__spinner__animation__keyframes { 1130 | 0% { 1131 | stroke-dashoffset: 140 1132 | } 1133 | 1134 | to { 1135 | stroke-dashoffset: 0 1136 | } 1137 | } 1138 | 1139 | @keyframes frontgridfield__spinner__animation__keyframes { 1140 | 0% { 1141 | stroke-dashoffset: 140 1142 | } 1143 | 1144 | to { 1145 | stroke-dashoffset: 0 1146 | } 1147 | } 1148 | 1149 | @-webkit-keyframes frontgridfield__spinner__animation__keyframes_ie { 1150 | 0% { 1151 | opacity: 0 1152 | } 1153 | 1154 | to { 1155 | opacity: 1 1156 | } 1157 | } 1158 | 1159 | @-o-keyframes frontgridfield__spinner__animation__keyframes_ie { 1160 | 0% { 1161 | opacity: 0 1162 | } 1163 | 1164 | to { 1165 | opacity: 1 1166 | } 1167 | } 1168 | 1169 | @keyframes frontgridfield__spinner__animation__keyframes_ie { 1170 | 0% { 1171 | opacity: 0 1172 | } 1173 | 1174 | to { 1175 | opacity: 1 1176 | } 1177 | } 1178 | -------------------------------------------------------------------------------- /images/arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/arrows.png -------------------------------------------------------------------------------- /images/btn-icon/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/btn-icon/cross.png -------------------------------------------------------------------------------- /images/btn-icon/document--pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/btn-icon/document--pencil.png -------------------------------------------------------------------------------- /images/btn-icon/ellipsis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/btn-icon/ellipsis.png -------------------------------------------------------------------------------- /images/btn-icon/magnifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/btn-icon/magnifier.png -------------------------------------------------------------------------------- /images/icons/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/icons/add.png -------------------------------------------------------------------------------- /images/icons/chain--minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/icons/chain--minus.png -------------------------------------------------------------------------------- /images/icons/chain--plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/icons/chain--plus.png -------------------------------------------------------------------------------- /images/icons/cross-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/icons/cross-circle.png -------------------------------------------------------------------------------- /images/icons/download-csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/icons/download-csv.png -------------------------------------------------------------------------------- /images/icons/filter-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/icons/filter-icons.png -------------------------------------------------------------------------------- /images/icons/pagination-arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webbuilders-group/silverstripe-frontendgridfield/84f4bf6a0958960cc364d3fa1f7ffe5208155555/images/icons/pagination-arrows.png -------------------------------------------------------------------------------- /javascript/FrontEndGridField.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $.entwine('ss', function($) { 3 | $('.ss-gridfield').entwine({ 4 | UUID: null, 5 | 6 | onmatch: function() { 7 | this._super(); 8 | this.setUUID(new Date().getTime()); 9 | }, 10 | 11 | closeDialog: function() { 12 | var dialog=$('#ss-ui-dialog-'+(this.getUUID())); 13 | 14 | if(dialog.length>0) { 15 | dialog.fegfdialog('close'); 16 | } 17 | } 18 | }); 19 | 20 | //Init jQuery UI Buttons 21 | $('.ss-gridfield .ss-ui-button, .ss-gridfield .action, .ss-gridfield .trigger').entwine({ 22 | onadd: function() { 23 | this.addClass('ss-ui-button'); 24 | if(!this.data('button')) this.button(); 25 | this._super(); 26 | }, 27 | onremove: function() { 28 | if(this.data('button')) this.button('destroy'); 29 | this._super(); 30 | } 31 | }); 32 | 33 | 34 | $('.frontendgrid.ss-gridfield:not(.ss-gridfield-editable) .grid-field__col-compact .action.gridfield-button-delete, .frontendgrid.ss-gridfield:not(.ss-gridfield-editable) .col-buttons .action--delete').entwine({ 35 | /** 36 | * Function: onclick 37 | */ 38 | onclick: function(e) { 39 | // Confirmation on delete 40 | if(!confirm(ss.i18n._t('Admin.DELETECONFIRMMESSAGE', 'Are you sure you want to delete this record?'))) { 41 | e.preventDefault(); 42 | return false; 43 | } 44 | 45 | if(!this.is(':disabled')) { 46 | var filterState='show'; //filterstate should equal current state. 47 | 48 | if(this.hasClass('ss-gridfield-button-close') || !(this.closest('.ss-gridfield').hasClass('show-filter'))){ 49 | filterState='hidden'; 50 | } 51 | 52 | this.getGridField().reload({data: [{name: this.attr('name'), value: this.val(), filter: filterState}]}); 53 | } 54 | 55 | e.preventDefault(); 56 | return false; 57 | } 58 | }); 59 | 60 | 61 | //Row Click 62 | $('.ss-gridfield:not(.ss-gridfield-editable) .ss-gridfield-item:not(.ss-gridfield-no-items) td:not(.grid-field__col-compact)').entwine({ 63 | /** 64 | * Function: onclick 65 | */ 66 | onclick: function(e) { 67 | var editButton=$(this).parent().find('a.edit-link, a.view-link'); 68 | if (editButton.length) { 69 | var self=this, id='ss-ui-dialog-'+this.getGridField().getUUID(); 70 | var dialog=$('#'+id); 71 | 72 | if(!dialog.length) { 73 | dialog=$('
'); 74 | $('body').append(dialog); 75 | } 76 | 77 | var extraClass=(this.data('popupclass') ? this.data('popupclass'):''); 78 | dialog.fegfdialog({ 79 | title: editButton.text(), 80 | iframeUrl: editButton.attr('href'), 81 | autoOpen: true, 82 | dialogExtraClass: extraClass, 83 | close: function(e, ui) { 84 | self.getGridField().reload(); 85 | } 86 | }); 87 | } 88 | e.preventDefault(); 89 | return false; 90 | } 91 | }); 92 | $('.ss-gridfield:not(.ss-gridfield-editable) .ss-gridfield-item:not(.ss-gridfield-no-items) td.grid-field__col-compact').entwine({ 93 | /** 94 | * Function: onclick 95 | */ 96 | onclick: function(e) { 97 | e.preventDefault(); 98 | return false; 99 | } 100 | }); 101 | 102 | //View/Edit Button Click 103 | $('.ss-gridfield a.edit-link, .ss-gridfield a.view-link, .ss-gridfield a.new-link').entwine({ 104 | /** 105 | * Function: onclick 106 | */ 107 | onclick: function(e) { 108 | var self=this, id='ss-ui-dialog-'+this.getGridField().getUUID(); 109 | var dialog=$('#'+id); 110 | 111 | if(!dialog.length) { 112 | dialog=$('
'); 113 | $('body').append(dialog); 114 | } 115 | 116 | var extraClass=(this.data('popupclass') ? this.data('popupclass'):''); 117 | dialog.fegfdialog({ 118 | title: $(this).text(), 119 | iframeUrl: this.attr('href'), 120 | autoOpen: true, 121 | dialogExtraClass: extraClass, 122 | close: function(e, ui) { 123 | self.getGridField().reload(); 124 | } 125 | }); 126 | 127 | e.preventDefault(); 128 | return false; 129 | } 130 | }); 131 | 132 | $('.grid-field__table .ss-gridfield-item.loading').entwine({ 133 | onmatch: function() { 134 | this.append('
\ 135 |
\ 136 |
\ 137 |
\ 138 | \ 146 | \ 147 | \ 148 | \ 158 | \ 159 | \ 160 | \ 161 | \ 162 | \ 166 | \ 173 | \ 174 | \ 175 |
\ 176 |
\ 177 |
'); 178 | 179 | this._super(); 180 | }, 181 | 182 | onunmatch: function() { 183 | this.children('.cms-loading-container').remove(); 184 | 185 | this._super(); 186 | } 187 | }); 188 | }); 189 | 190 | $.widget("fegf.fegfdialog", $.ssui.ssdialog, { 191 | _resizeIframe: function() { 192 | //Call Parent 193 | $.ssui.ssdialog.prototype._resizeIframe.call(this); 194 | 195 | var iframe=this.element.children('iframe'); 196 | var titlebar=jQuery(this.uiDialog).find('.ui-dialog-titlebar'); 197 | 198 | //Resize the iframe taking into account the title bar 199 | if(titlebar.length>0 && titlebar.is(':visible')) { 200 | iframe.attr('height', iframe.attr('height')-titlebar.outerHeight()); 201 | } 202 | } 203 | }); 204 | })(jQuery); -------------------------------------------------------------------------------- /javascript/GridField.js: -------------------------------------------------------------------------------- 1 | /*** Copied and modified from https://github.com/silverstripe/silverstripe-admin/tree/1.4 ***/ 2 | (function($) { 3 | $.entwine('ss', function($) { 4 | $('.grid-field').entwine({ 5 | onmatch: function () { 6 | if (this.needsColumnFix()) { 7 | this.fixColumns(); 8 | this.injectSearchButton(false); 9 | } 10 | 11 | if (this.is('.grid-field--lazy-loadable') && ( 12 | (this.closest('.ss-tabset').length === 0) || (this.data('gridfield-lazy-load-state') === 'force') ) 13 | ) { 14 | // If our GridField is not inside a tabset for an immidiate reload 15 | this.data('gridfield-lazy-load-state', 'ready'); 16 | this.lazyload(); 17 | } 18 | 19 | this.data('gridfield-lazy-load-state', 'ready'); 20 | }, 21 | 22 | /** 23 | * @func Trigger a lazy load on this gridfield 24 | */ 25 | lazyload: function() { 26 | if (this.data('gridfield-lazy-load-state') !== 'ready') { 27 | this.data('gridfield-lazy-load-state', 'force'); 28 | } else { 29 | this.removeClass('grid-field--lazy-loadable').addClass('grid-field--lazy-loaded'); 30 | this.reload(); 31 | } 32 | 33 | }, 34 | 35 | /** 36 | * @param {Object} Additional options for jQuery.ajax() call 37 | * @param {successCallback} callback to call after reloading succeeded. 38 | */ 39 | reload: function(ajaxOpts, successCallback) { 40 | var self = this, form = this.closest('form'), 41 | focusedElName = this.find(':input:focus').attr('name'), // Save focused element for restoring after refresh 42 | data = form.find(':input:not(.grid-field__search-holder :input, .relation-search)').serializeArray(), 43 | tbody = this.find('tbody'), 44 | colspan = this.find('.grid-field__title-row th').attr('colspan'); 45 | ; 46 | 47 | if(!ajaxOpts) ajaxOpts = {}; 48 | if(!ajaxOpts.data) ajaxOpts.data = []; 49 | ajaxOpts.data = ajaxOpts.data.concat(data); 50 | 51 | 52 | // Include any GET parameters from the current URL, as the view state might depend on it. 53 | // For example, a list prefiltered through external search criteria might be passed to GridField. 54 | if(window.location.search) { 55 | ajaxOpts.data = window.location.search.replace(/^\?/, '') + '&' + $.param(ajaxOpts.data); 56 | } 57 | 58 | // Enable loading animation 59 | tbody.find('tr').remove(); 60 | var loadingCell = $('') 61 | .addClass('ss-gridfield-item loading') 62 | .attr('colspan', colspan); 63 | tbody.append($('').append(loadingCell)); 64 | 65 | var request = $.ajax($.extend({}, { 66 | headers: {"X-Pjax" : 'CurrentField'}, 67 | type: "POST", 68 | url: this.data('url'), 69 | dataType: 'html', 70 | success: function (data) { 71 | // Replace the grid field with response, not the form. 72 | // TODO Only replaces all its children, to avoid replacing the current scope 73 | // of the executing method. Means that it doesn't retrigger the onmatch() on the main container. 74 | self.empty().append($(data).children()); 75 | 76 | // Refocus previously focused element. Useful e.g. for finding+adding 77 | // multiple relationships via keyboard. 78 | if(focusedElName) self.find(':input[name="' + focusedElName + '"]').focus(); 79 | 80 | // Update filter 81 | if (self.find('.grid-field__filter-header, .grid-field__search-holder').length) { 82 | var visible = ajaxOpts.data[0].filter === "show"; 83 | if (self.needsColumnFix()) { 84 | self.fixColumns(); 85 | } 86 | self.injectSearchButton(visible); 87 | } 88 | 89 | if(successCallback) successCallback.apply(this, arguments); 90 | self.trigger('reload', self); 91 | 92 | // Trigger change if it's not specifically supressed 93 | if (ajaxOpts.data[0].triggerChange !== false) { 94 | self.trigger('change'); 95 | } 96 | }, 97 | error: function(e) { 98 | alert(i18n._t('Admin.ERRORINTRANSACTION')); 99 | }, 100 | complete: function(request, status) { 101 | self.find('.loading').removeClass('loading'); 102 | } 103 | }, ajaxOpts)); 104 | }, 105 | showDetailView: function(url) { 106 | window.location.href = url; 107 | }, 108 | getItems: function() { 109 | return this.find('.ss-gridfield-item'); 110 | }, 111 | /** 112 | * @param {String} 113 | * @param {Mixed} 114 | */ 115 | setState: function(k, v) { 116 | var state = this.getState(); 117 | state[k] = v; 118 | this.find(':input[name="' + this.data('name') + '[GridState]"]').val(JSON.stringify(state)); 119 | }, 120 | /** 121 | * @return {Object} 122 | */ 123 | getState: function() { 124 | return JSON.parse(this.find(':input[name="' + this.data('name') + '[GridState]"]').val()); 125 | }, 126 | 127 | needsColumnFix: function() { 128 | return ( 129 | this.find('.grid-field__filter-header, .grid-field__search-holder').length && 130 | !this.find('.grid-field__col-compact').length && 131 | !this.find('th.col-Actions').length 132 | ); 133 | }, 134 | 135 | fixColumns: function (visible) { 136 | this.find('.sortable-header').append(''); 137 | this.find('tbody tr').each(function () { 138 | var cell = $(this).find('td:last'); 139 | cell.attr('colspan', 2); 140 | }); 141 | var $extraCell = $(''); 142 | $('.grid-field__filter-header th:last .action').each(function() { 143 | $(this).detach(); 144 | $extraCell.append($(this)); 145 | }); 146 | $('.grid-field__filter-header').append($extraCell); 147 | }, 148 | 149 | injectSearchButton: function(visible) { 150 | const hasLegacyFilterHeader = this.find('.grid-field__filter-header').length > 0; 151 | let content; 152 | if (visible) { 153 | content = ''; 154 | this.addClass('show-filter').find('.grid-field__filter-header, .grid-field__search-holder').removeClass('grid-field__search-holder--hidden'); 155 | if (!hasLegacyFilterHeader) { 156 | this.find(':button[name=showFilter]').hide(); 157 | } 158 | } else { 159 | content = ''; 160 | this.removeClass('show-filter').find('.grid-field__filter-header, .grid-field__search-holder').addClass('grid-field__search-holder--hidden'); 161 | } 162 | if (hasLegacyFilterHeader) { 163 | this.find('.sortable-header th:last').html(content); 164 | } 165 | } 166 | }); 167 | 168 | $('.grid-field *').entwine({ 169 | getGridField: function() { 170 | return this.closest('.grid-field'); 171 | } 172 | }); 173 | 174 | 175 | $('.grid-field :button[name=showFilter]').entwine({ 176 | onclick: function(e) { 177 | this.closest('.grid-field') 178 | .find('.grid-field__filter-header, .grid-field__search-holder') 179 | .removeClass('grid-field__search-holder--hidden') 180 | .find(':input:first').focus(); // focus first search field 181 | 182 | this.closest('.grid-field').addClass('show-filter'); 183 | this.parent().html(''); 184 | e.preventDefault(); 185 | } 186 | }); 187 | 188 | 189 | $('.grid-field .ss-gridfield-item').entwine({ 190 | onclick: function (e) { 191 | if (e.target.classList.contains('action-menu__toggle')) { 192 | this._super(e); 193 | return false; 194 | } 195 | 196 | if($(e.target).closest('.action').length) { 197 | this._super(e); 198 | return false; 199 | } 200 | 201 | var formLink = this.find('.edit-link, .view-link'); 202 | if(formLink.length) this.getGridField().showDetailView(formLink.prop('href')); 203 | }, 204 | onmouseover: function() { 205 | if(this.find('.edit-link, .view-link').length) this.css('cursor', 'pointer'); 206 | }, 207 | onmouseout: function() { 208 | this.css('cursor', 'default'); 209 | } 210 | }); 211 | 212 | $('.grid-field .action.action_import:button').entwine({ 213 | onclick: function(e) { 214 | e.preventDefault(); 215 | this.openmodal(); 216 | }, 217 | onmatch: function() { 218 | this._super(); 219 | // Trigger auto-open 220 | if (this.data('state') === 'open') { 221 | this.openmodal(); 222 | } 223 | }, 224 | onunmatch: function() { 225 | this._super(); 226 | }, 227 | 228 | openmodal: function() { 229 | // Remove existing modal 230 | let modal = $(this.data('target')); 231 | let newModal = $(this.data('modal')); 232 | if (modal.length < 1) { 233 | // Add modal to end of body tag 234 | modal = newModal; 235 | modal.appendTo(document.body); 236 | } else { 237 | // Replace inner content 238 | modal.innerHTML = newModal.innerHTML; 239 | } 240 | 241 | // Apply backdrop 242 | let backdrop = $('.modal-backdrop'); 243 | if(backdrop.length < 1) { 244 | backdrop = $(''); 245 | backdrop.appendTo(document.body); 246 | } 247 | 248 | function closeModal() { 249 | backdrop.removeClass('show'); 250 | modal.removeClass('show'); 251 | setTimeout(function() { 252 | backdrop.remove(); 253 | }, 150) // Simulate the bootstrap out-transition period 254 | } 255 | 256 | // Set close action 257 | modal.find('[data-dismiss]').add('.modal-backdrop') 258 | .on('click', function () { 259 | closeModal(); 260 | }); 261 | 262 | $(document).on('keydown', function(e) { 263 | if (e.keyCode === 27) { // Escape key 264 | closeModal(); 265 | } 266 | }); 267 | 268 | // Fade each element in (use setTimeout to ensure initial render at opacity=0 works) 269 | setTimeout(function() { 270 | backdrop.addClass('show'); 271 | modal.addClass('show'); 272 | }, 0); 273 | 274 | } 275 | }); 276 | 277 | $('.grid-field .action:button').entwine({ 278 | onclick: function (e) { 279 | var filterState = 'show'; //filterstate should equal current state. 280 | let triggerChange = true; 281 | 282 | // If the button is disabled, do nothing. 283 | if (this.is(':disabled')) { 284 | e.preventDefault(); 285 | return; 286 | } 287 | 288 | if(this.hasClass('ss-gridfield-button-close') || !(this.closest('.grid-field').hasClass('show-filter'))) { 289 | filterState = 'hidden'; 290 | } 291 | 292 | if (this.hasClass('ss-gridfield-pagination-action') || this.hasClass('grid-field__sort')) { 293 | triggerChange = false; 294 | } 295 | 296 | const successCallback = function(data, status, response) { 297 | const messageText = response.getResponseHeader('X-Message-Text'); 298 | const messageType = response.getResponseHeader('X-Message-Type'); 299 | if (messageText && messageType) { 300 | var formEditError = $("#Form_EditForm_error"); 301 | formEditError.addClass(messageType); 302 | formEditError.html(messageText); 303 | formEditError.show(); 304 | } 305 | }; 306 | 307 | var data = [ 308 | { 309 | name: this.attr('name'), 310 | value: this.val(), 311 | filter: filterState, 312 | triggerChange: triggerChange 313 | }, 314 | ]; 315 | 316 | var actionState = this.data('action-state'); 317 | if (actionState) { 318 | data.push({ 319 | name: 'ActionState', 320 | value: JSON.stringify(actionState), 321 | }); 322 | } 323 | 324 | this.getGridField().reload( 325 | { data: data }, 326 | successCallback 327 | ); 328 | 329 | e.preventDefault(); 330 | }, 331 | /** 332 | * Get the url this action should submit to 333 | */ 334 | actionurl: function () { 335 | var btn = this.closest(':button'), grid = this.getGridField(), 336 | form = this.closest('form'), data = form.find(':input.gridstate').serialize(), 337 | csrf = form.find('input[name="SecurityID"]').val(); 338 | 339 | // Add current button 340 | data += "&" + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val()); 341 | 342 | // Add csrf 343 | if(csrf) { 344 | data += "&SecurityID=" + encodeURIComponent(csrf); 345 | } 346 | 347 | // Add action data 348 | var actionState = this.data('action-state'); 349 | if (actionState) { 350 | data += '&ActionState=' + encodeURIComponent(JSON.stringify(actionState)) 351 | } 352 | 353 | // Include any GET parameters from the current URL, as the view 354 | // state might depend on it. For example, a list pre-filtered 355 | // through external search criteria might be passed to GridField. 356 | if(window.location.search) { 357 | data = window.location.search.replace(/^\?/, '') + '&' + data; 358 | } 359 | 360 | // decide whether we should use ? or & to connect the URL 361 | var connector = grid.data('url').indexOf('?') == -1 ? '?' : '&'; 362 | 363 | return jQuery.path.makeUrlAbsolute( 364 | grid.data('url') + connector + data, 365 | $('base').attr('href') 366 | ); 367 | } 368 | 369 | }); 370 | 371 | /** 372 | * Don't allow users to submit empty values in grid field auto complete inputs. 373 | */ 374 | $('.grid-field .add-existing-autocompleter').entwine({ 375 | onbuttoncreate: function () { 376 | var self = this; 377 | 378 | this.toggleDisabled(); 379 | 380 | this.find('input[type="text"]').on('keyup', function () { 381 | self.toggleDisabled(); 382 | }); 383 | }, 384 | onunmatch: function () { 385 | this.find('input[type="text"]').off('keyup'); 386 | }, 387 | toggleDisabled: function () { 388 | var $button = this.find('.ss-ui-button'), 389 | $input = this.find('input[type="text"]'), 390 | inputHasValue = $input.val() !== '', 391 | buttonDisabled = $button.is(':disabled'); 392 | 393 | if ((inputHasValue && buttonDisabled) || (!inputHasValue && !buttonDisabled)) { 394 | $button.button("option", "disabled", !buttonDisabled); 395 | } 396 | } 397 | }); 398 | 399 | // Covers both tabular delete button, and the button on the detail form 400 | $('.grid-field .grid-field__col-compact .action--delete, .grid-field .grid-field__col-compact .action--archive, .cms-edit-form .btn-toolbar .action--delete, .cms-edit-form .btn-toolbar .action--archive').entwine({ 401 | onclick: function(e){ 402 | const confirmMessage = $(this).hasClass('action--archive') 403 | ? i18n._t('Admin.ARCHIVECONFIRMMESSAGE', 'Are you sure you want to archive this record?') 404 | : i18n._t('Admin.DELETECONFIRMMESSAGE', 'Are you sure you want to delete this record?'); 405 | 406 | if (!confirm(confirmMessage)) { 407 | e.preventDefault(); 408 | return false; 409 | } else { 410 | this._super(e); 411 | } 412 | } 413 | }); 414 | 415 | $('.grid-field .grid-print-button.action:button').entwine({ 416 | UUID: null, 417 | onmatch: function() { 418 | this._super(); 419 | this.setUUID(new Date().getTime()); 420 | }, 421 | onunmatch: function() { 422 | this._super(); 423 | }, 424 | onclick: function(e) { 425 | var url = this.actionurl(); 426 | window.open(url); 427 | e.preventDefault(); 428 | return false; 429 | } 430 | }); 431 | 432 | $('.ss-gridfield-print-iframe').entwine({ 433 | onmatch: function(){ 434 | this._super(); 435 | 436 | this.hide().bind('load', function() { 437 | this.focus(); 438 | var ifWin = this.contentWindow || this; 439 | ifWin.print(); 440 | }); 441 | }, 442 | onunmatch: function() { 443 | this._super(); 444 | } 445 | }); 446 | 447 | /** 448 | * Prevents actions from causing an ajax reload of the field. 449 | * 450 | * Useful e.g. for actions which rely on HTTP response headers being 451 | * interpreted natively by the browser, like file download triggers. 452 | */ 453 | $('.grid-field .action.no-ajax, .grid-field .no-ajax .action:button').entwine({ 454 | onclick: function(e){ 455 | window.location.href = this.actionurl(); 456 | e.preventDefault(); 457 | return false; 458 | } 459 | }); 460 | 461 | $('.grid-field .action-detail').entwine({ 462 | onclick: function() { 463 | this.getGridField().showDetailView($(this).prop('href')); 464 | return false; 465 | } 466 | }); 467 | 468 | /** 469 | * Allows selection of one or more rows in the grid field. 470 | * Purely clientside at the moment. 471 | */ 472 | $('.grid-field[data-selectable]').entwine({ 473 | /** 474 | * @return {jQuery} Collection 475 | */ 476 | getSelectedItems: function() { 477 | return this.find('.ss-gridfield-item.ui-selected'); 478 | }, 479 | /** 480 | * @return {Array} Of record IDs 481 | */ 482 | getSelectedIDs: function() { 483 | return $.map(this.getSelectedItems(), function(el) {return $(el).data('id');}); 484 | } 485 | }); 486 | $('.grid-field[data-selectable] .ss-gridfield-items').entwine({ 487 | onadd: function() { 488 | this._super(); 489 | 490 | // TODO Limit to single selection 491 | this.selectable(); 492 | }, 493 | onremove: function() { 494 | this._super(); 495 | if (this.data('selectable')) this.selectable('destroy'); 496 | } 497 | }); 498 | 499 | /** 500 | * Catch submission event in filter input fields, and submit the correct button 501 | * rather than the whole form. 502 | */ 503 | $('.grid-field .grid-field__filter-header :input').entwine({ 504 | onmatch: function() { 505 | var filterbtn = this.closest('.extra').find('.ss-gridfield-button-filter'), 506 | resetbtn = this.closest('.extra').find('.ss-gridfield-button-reset'); 507 | 508 | if(this.val()) { 509 | filterbtn.addClass('filtered'); 510 | resetbtn.addClass('filtered'); 511 | } 512 | this._super(); 513 | }, 514 | onunmatch: function() { 515 | this._super(); 516 | }, 517 | onkeydown: function(e) { 518 | // Skip reset button events, they should trigger default submission 519 | if(this.closest('.ss-gridfield-button-reset').length) return; 520 | 521 | var filterbtn = this.closest('.extra').find('.ss-gridfield-button-filter'), 522 | resetbtn = this.closest('.extra').find('.ss-gridfield-button-reset'); 523 | 524 | if(e.keyCode == '13') { 525 | var btns = this.closest('.grid-field__filter-header').find('button.ss-gridfield-button-filter'); 526 | var filterState='show'; //filterstate should equal current state. 527 | if(this.hasClass('ss-gridfield-button-close')||!(this.closest('.grid-field').hasClass('show-filter'))){ 528 | filterState='hidden'; 529 | } 530 | 531 | var ajaxData = [{ 532 | name: btns.attr('name'), 533 | value: btns.val(), 534 | filter: filterState, 535 | triggerChange: false 536 | }]; 537 | 538 | if (btns.data('action-state')) { 539 | ajaxData.push({ 540 | name: 'ActionState', 541 | value: JSON.stringify(btns.data('action-state')), 542 | }); 543 | } 544 | 545 | this.getGridField().reload({ 546 | data: ajaxData 547 | }); 548 | return false; 549 | }else{ 550 | filterbtn.addClass('hover-alike'); 551 | resetbtn.addClass('hover-alike'); 552 | } 553 | } 554 | }); 555 | 556 | $(".grid-field .relation-search").entwine({ 557 | onfocusin: function (event) { 558 | this.autocomplete({ 559 | source: function(request, response){ 560 | var searchField = $(this.element); 561 | var form = $(this.element).closest("form"); 562 | $.ajax({ 563 | headers: { 564 | "X-Pjax" : 'Partial' 565 | }, 566 | dataType: 'json', 567 | type: "GET", 568 | url: $(searchField).data('searchUrl'), 569 | data: encodeURIComponent(searchField.attr('name'))+'='+encodeURIComponent(searchField.val()), 570 | success: response, 571 | error: function(e) { 572 | alert(i18n._t('Admin.ERRORINTRANSACTION', 'An error occured while fetching data from the server\n Please try again later.')); 573 | } 574 | }); 575 | }, 576 | select: function(event, ui) { 577 | var hiddenField = $(''); 578 | hiddenField.val(ui.item.id); 579 | $(this) 580 | .closest(".grid-field") 581 | .find(".action_gridfield_relationfind") 582 | .replaceWith(hiddenField); 583 | var addbutton = $(this).closest(".grid-field").find(".action_gridfield_relationadd"); 584 | 585 | addbutton.removeAttr('disabled'); 586 | } 587 | }); 588 | } 589 | }); 590 | 591 | $(".grid-field .pagination-page-number input").entwine({ 592 | onkeydown: function(event) { 593 | if(event.keyCode == 13) { 594 | event.preventDefault(); 595 | var newpage = parseInt($(this).val(), 10); 596 | 597 | var gridfield = $(this).getGridField(); 598 | gridfield.setState('GridFieldPaginator', {currentPage: newpage}); 599 | gridfield.reload(); 600 | 601 | return false; 602 | } 603 | } 604 | }); 605 | }); 606 | })(jQuery); -------------------------------------------------------------------------------- /javascript/boot.template.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var configDefault = { 3 | SecurityID: '$SecurityID', 4 | absoluteBaseUrl: '$AbsoluteBaseURL', 5 | baseUrl: '$BaseURL', 6 | adminUrl: 'admin/', 7 | environment: '$Environment', 8 | debugging: $Debugging, 9 | sections: [ 10 | { 11 | name: 'SilverStripe\\Admin\\LeftAndMain', 12 | url: 'admin', 13 | graphql: { 14 | cachedTypenames: false, 15 | }, 16 | }, 17 | ], 18 | }; 19 | 20 | window.ss = window.ss || {}; 21 | window.ss.config = window.ss.config || configDefault; 22 | })(); -------------------------------------------------------------------------------- /javascript/externals/hafriedlander/jquery-entwine/jquery.entwine-dist.js: -------------------------------------------------------------------------------- 1 | /* jQuery.Entwine - Copyright 2009-2011 Hamish Friedlander and SilverStripe. Version . */ 2 | 3 | /* vendor/jquery.selector/jquery.class.js */ 4 | 5 | /** 6 | * Very basic Class utility. Based on base and jquery.class. 7 | * 8 | * Class definition: var Foo = Base.extend({ init: function(){ Constructor }; method_name: function(){ Method } }); 9 | * 10 | * Inheritance: var Bar = Foo.extend({ method_name: function(){ this._super(); } }); 11 | * 12 | * new-less Constructor: new Foo(arg) <-same as-> Foo(arg) 13 | */ 14 | 15 | var Base; 16 | 17 | (function(){ 18 | 19 | var marker = {}, fnTest = /xyz/.test(function(){var xyz;}) ? /\b_super\b/ : /.*/; 20 | 21 | // The base Class implementation (does nothing) 22 | Base = function(){}; 23 | 24 | Base.addMethod = function(name, func) { 25 | var parent = this._super && this._super.prototype; 26 | 27 | if (parent && fnTest.test(func)) { 28 | this.prototype[name] = function(){ 29 | var tmp = this._super; 30 | this._super = parent[name]; 31 | try { 32 | var ret = func.apply(this, arguments); 33 | } 34 | finally { 35 | this._super = tmp; 36 | } 37 | return ret; 38 | }; 39 | } 40 | else this.prototype[name] = func; 41 | }; 42 | 43 | Base.addMethods = function(props) { 44 | for (var name in props) { 45 | if (typeof props[name] == 'function') this.addMethod(name, props[name]); 46 | else this.prototype[name] = props[name]; 47 | } 48 | }; 49 | 50 | Base.subclassOf = function(parentkls) { 51 | var kls = this; 52 | while (kls) { 53 | if (kls === parentkls) return true; 54 | kls = kls._super; 55 | } 56 | }; 57 | 58 | // Create a new Class that inherits from this class 59 | Base.extend = function(props) { 60 | 61 | // The dummy class constructor 62 | var Kls = function() { 63 | if (arguments[0] === marker) return; 64 | 65 | if (this instanceof Kls) { 66 | if (this.init) this.init.apply(this, arguments); 67 | } 68 | else { 69 | var ret = new Kls(marker); if (ret.init) ret.init.apply(ret, arguments); return ret; 70 | } 71 | }; 72 | 73 | // Add the common class variables and methods 74 | Kls.constructor = Kls; 75 | Kls.extend = Base.extend; 76 | Kls.addMethod = Base.addMethod; 77 | Kls.addMethods = Base.addMethods; 78 | Kls.subclassOf = Base.subclassOf; 79 | 80 | Kls._super = this; 81 | 82 | // Attach the parent object to the inheritance chain 83 | Kls.prototype = new this(marker); 84 | Kls.prototype.constructor = Kls; 85 | 86 | // Copy the properties over onto the new prototype 87 | Kls.addMethods(props); 88 | 89 | return Kls; 90 | }; 91 | })();; 92 | 93 | 94 | /* vendor/jquery.selector/jquery.selector.js */ 95 | 96 | (function($){ 97 | 98 | var tokens = { 99 | UNICODE: /\\[0-9a-f]{1,6}(?:\r\n|[ \n\r\t\f])?/, 100 | ESCAPE: /(?:UNICODE)|\\[^\n\r\f0-9a-f]/, 101 | NONASCII: /[^\x00-\x7F]/, 102 | NMSTART: /[_a-z]|(?:NONASCII)|(?:ESCAPE)/, 103 | NMCHAR: /[_a-z0-9-]|(?:NONASCII)|(?:ESCAPE)/, 104 | IDENT: /-?(?:NMSTART)(?:NMCHAR)*/, 105 | 106 | NL: /\n|\r\n|\r|\f/, 107 | 108 | STRING: /(?:STRING1)|(?:STRING2)|(?:STRINGBARE)/, 109 | STRING1: /"(?:(?:ESCAPE)|\\(?:NL)|[^\n\r\f\"])*"/, 110 | STRING2: /'(?:(?:ESCAPE)|\\(?:NL)|[^\n\r\f\'])*'/, 111 | STRINGBARE: /(?:(?:ESCAPE)|\\(?:NL)|[^\n\r\f\]])*/, 112 | 113 | FUNCTION: /(?:IDENT)\(\)/, 114 | 115 | INTEGER: /[0-9]+/, 116 | 117 | WITHN: /([-+])?(INTEGER)?(n)\s*(?:([-+])\s*(INTEGER))?/, 118 | WITHOUTN: /([-+])?(INTEGER)/ 119 | }; 120 | 121 | var rx = { 122 | not: /:not\(/, 123 | not_end: /\)/, 124 | 125 | tag: /((?:IDENT)|\*)/, 126 | id: /#(IDENT)/, 127 | cls: /\.(IDENT)/, 128 | attr: /\[\s*(IDENT)\s*(?:([^=]?=)\s*(STRING)\s*)?\]/, 129 | pseudo_el: /(?::(first-line|first-letter|before|after))|(?:::((?:FUNCTION)|(?:IDENT)))/, 130 | pseudo_cls_nth: /:nth-child\(\s*(?:(?:WITHN)|(?:WITHOUTN)|(odd|even))\s*\)/, 131 | pseudo_cls: /:(IDENT)/, 132 | 133 | comb: /\s*(\+|~|>)\s*|\s+/, 134 | comma: /\s*,\s*/, 135 | important: /\s+!important\s*$/ 136 | }; 137 | 138 | /* Replace placeholders with actual regex, and mark all as case insensitive */ 139 | var token = /[A-Z][A-Z0-9]+/; 140 | for (var k in rx) { 141 | var m, src = rx[k].source; 142 | while (m = src.match(token)) src = src.replace(m[0], tokens[m[0]].source); 143 | rx[k] = new RegExp(src, 'gi'); 144 | } 145 | 146 | /** 147 | * A string that matches itself against regexii, and keeps track of how much of itself has been matched 148 | */ 149 | var ConsumableString = Base.extend({ 150 | init: function(str) { 151 | this.str = str; 152 | this.pos = 0; 153 | }, 154 | match: function(rx) { 155 | var m; 156 | rx.lastIndex = this.pos; 157 | if ((m = rx.exec(this.str)) && m.index == this.pos ) { 158 | this.pos = rx.lastIndex ? rx.lastIndex : this.str.length ; 159 | return m; 160 | } 161 | return null; 162 | }, 163 | peek: function(rx) { 164 | var m; 165 | rx.lastIndex = this.pos; 166 | if ((m = rx.exec(this.str)) && m.index == this.pos ) return m; 167 | return null; 168 | }, 169 | showpos: function() { 170 | return this.str.slice(0,this.pos)+'' + this.str.slice(this.pos); 171 | }, 172 | done: function() { 173 | return this.pos == this.str.length; 174 | } 175 | }); 176 | 177 | /* A base class that all Selectors inherit off */ 178 | var SelectorBase = Base.extend({}); 179 | 180 | /** 181 | * A class representing a Simple Selector, as per the CSS3 selector spec 182 | */ 183 | var SimpleSelector = SelectorBase.extend({ 184 | init: function() { 185 | this.tag = null; 186 | this.id = null; 187 | this.classes = []; 188 | this.attrs = []; 189 | this.nots = []; 190 | this.pseudo_classes = []; 191 | this.pseudo_els = []; 192 | }, 193 | parse: function(selector) { 194 | var m; 195 | 196 | /* Pull out the initial tag first, if there is one */ 197 | if (m = selector.match(rx.tag)) this.tag = m[1]; 198 | 199 | /* Then for each selection type, try and find a match */ 200 | do { 201 | if (m = selector.match(rx.not)) { 202 | this.nots[this.nots.length] = SelectorsGroup().parse(selector); 203 | if (!(m = selector.match(rx.not_end))) { 204 | throw 'Invalid :not term in selector'; 205 | } 206 | } 207 | else if (m = selector.match(rx.id)) this.id = m[1]; 208 | else if (m = selector.match(rx.cls)) this.classes[this.classes.length] = m[1]; 209 | else if (m = selector.match(rx.attr)) this.attrs[this.attrs.length] = [ m[1], m[2], m[3] ]; 210 | else if (m = selector.match(rx.pseudo_el)) this.pseudo_els[this.pseudo_els.length] = m[1] || m[2]; 211 | else if (m = selector.match(rx.pseudo_cls_nth)) { 212 | if (m[3]) { 213 | var a = parseInt((m[1]||'')+(m[2]||'1')); 214 | var b = parseInt((m[4]||'')+(m[5]||'0')); 215 | } 216 | else { 217 | var a = m[8] ? 2 : 0; 218 | var b = m[8] ? (4-m[8].length) : parseInt((m[6]||'')+m[7]); 219 | } 220 | this.pseudo_classes[this.pseudo_classes.length] = ['nth-child', [a, b]]; 221 | } 222 | else if (m = selector.match(rx.pseudo_cls)) this.pseudo_classes[this.pseudo_classes.length] = [m[1]]; 223 | 224 | } while(m && !selector.done()); 225 | 226 | return this; 227 | } 228 | }); 229 | 230 | /** 231 | * A class representing a Selector, as per the CSS3 selector spec 232 | */ 233 | var Selector = SelectorBase.extend({ 234 | init: function(){ 235 | this.parts = []; 236 | }, 237 | parse: function(cons){ 238 | this.parts[this.parts.length] = SimpleSelector().parse(cons); 239 | 240 | while (!cons.done() && !cons.peek(rx.comma) && (m = cons.match(rx.comb))) { 241 | this.parts[this.parts.length] = m[1] || ' '; 242 | this.parts[this.parts.length] = SimpleSelector().parse(cons); 243 | } 244 | 245 | return this.parts.length == 1 ? this.parts[0] : this; 246 | } 247 | }); 248 | 249 | /** 250 | * A class representing a sequence of selectors, as per the CSS3 selector spec 251 | */ 252 | var SelectorsGroup = SelectorBase.extend({ 253 | init: function(){ 254 | this.parts = []; 255 | }, 256 | parse: function(cons){ 257 | this.parts[this.parts.length] = Selector().parse(cons); 258 | 259 | while (!cons.done() && (m = cons.match(rx.comma))) { 260 | this.parts[this.parts.length] = Selector().parse(cons); 261 | } 262 | 263 | return this.parts.length == 1 ? this.parts[0] : this; 264 | } 265 | }); 266 | 267 | 268 | $.selector = function(s){ 269 | var cons = ConsumableString(s); 270 | var res = SelectorsGroup().parse(cons); 271 | 272 | res.selector = s; 273 | 274 | if (!cons.done()) throw 'Could not parse selector - ' + cons.showpos() ; 275 | else return res; 276 | }; 277 | 278 | $.selector.SelectorBase = SelectorBase; 279 | $.selector.SimpleSelector = SimpleSelector; 280 | $.selector.Selector = Selector; 281 | $.selector.SelectorsGroup = SelectorsGroup; 282 | 283 | })(jQuery); 284 | ; 285 | 286 | 287 | /* vendor/jquery.selector/jquery.selector.specifity.js */ 288 | 289 | (function($) { 290 | 291 | $.selector.SimpleSelector.addMethod('specifity', function() { 292 | if (this.spec) return this.spec; 293 | 294 | var spec = [ 295 | this.id ? 1 : 0, 296 | this.classes.length + this.attrs.length + this.pseudo_classes.length, 297 | ((this.tag && this.tag != '*') ? 1 : 0) + this.pseudo_els.length 298 | ]; 299 | $.each(this.nots, function(i,not){ 300 | var ns = not.specifity(); spec[0] += ns[0]; spec[1] += ns[1]; spec[2] += ns[2]; 301 | }); 302 | 303 | return this.spec = spec; 304 | }); 305 | 306 | $.selector.Selector.addMethod('specifity', function(){ 307 | if (this.spec) return this.spec; 308 | 309 | var spec = [0,0,0]; 310 | $.each(this.parts, function(i,part){ 311 | if (i%2) return; 312 | var ps = part.specifity(); spec[0] += ps[0]; spec[1] += ps[1]; spec[2] += ps[2]; 313 | }); 314 | 315 | return this.spec = spec; 316 | }); 317 | 318 | $.selector.SelectorsGroup.addMethod('specifity', function(){ 319 | if (this.spec) return this.spec; 320 | 321 | var spec = [0,0,0]; 322 | $.each(this.parts, function(i,part){ 323 | var ps = part.specifity(); spec[0] += ps[0]; spec[1] += ps[1]; spec[2] += ps[2]; 324 | }); 325 | 326 | return this.spec = spec; 327 | }); 328 | 329 | 330 | })(jQuery); 331 | ; 332 | 333 | 334 | /* vendor/jquery.selector/jquery.selector.matches.js */ 335 | 336 | /* 337 | This attempts to do the opposite of Sizzle. 338 | Sizzle is good for finding elements for a selector, but not so good for telling if an individual element matches a selector 339 | */ 340 | 341 | (function($) { 342 | 343 | /**** CAPABILITY TESTS ****/ 344 | var div = document.createElement('div'); 345 | div.innerHTML = '
'; 346 | 347 | // In IE 6-7, getAttribute often does the wrong thing (returns similar to el.attr), so we need to use getAttributeNode on that browser 348 | var getAttributeDodgy = div.firstChild.getAttribute('id') !== 'test'; 349 | 350 | // Does browser support Element.firstElementChild, Element.previousElementSibling, etc. 351 | var hasElementTraversal = div.firstElementChild && div.firstElementChild.tagName == 'FORM'; 352 | 353 | // Does browser support Element.children 354 | var hasChildren = div.children && div.children[0].tagName == 'FORM'; 355 | 356 | /**** INTRO ****/ 357 | 358 | var GOOD = /GOOD/g; 359 | var BAD = /BAD/g; 360 | 361 | var STARTS_WITH_QUOTES = /^['"]/g; 362 | 363 | var join = function(js) { 364 | return js.join('\n'); 365 | }; 366 | 367 | var join_complex = function(js) { 368 | var code = new String(js.join('\n')); // String objects can have properties set. strings can't 369 | code.complex = true; 370 | return code; 371 | }; 372 | 373 | /**** ATTRIBUTE ACCESSORS ****/ 374 | 375 | // Not all attribute names can be used as identifiers, so we encode any non-acceptable characters as hex 376 | var varForAttr = function(attr) { 377 | return '_' + attr.replace(/^[^A-Za-z]|[^A-Za-z0-9]/g, function(m){ return '_0x' + m.charCodeAt(0).toString(16) + '_'; }); 378 | }; 379 | 380 | var getAttr; 381 | 382 | // Good browsers 383 | if (!getAttributeDodgy) { 384 | getAttr = function(attr){ return 'var '+varForAttr(attr)+' = el.getAttribute("'+attr+'");' ; }; 385 | } 386 | // IE 6, 7 387 | else { 388 | // On IE 6 + 7, getAttribute still has to be called with DOM property mirror name, not attribute name. Map attributes to those names 389 | var getAttrIEMap = { 'class': 'className', 'for': 'htmlFor' }; 390 | 391 | getAttr = function(attr) { 392 | var ieattr = getAttrIEMap[attr] || attr; 393 | return 'var '+varForAttr(attr)+' = el.getAttribute("'+ieattr+'",2) || (el.getAttributeNode("'+attr+'")||{}).nodeValue;'; 394 | }; 395 | } 396 | 397 | /**** ATTRIBUTE COMPARITORS ****/ 398 | 399 | var attrchecks = { 400 | '-': '!K', 401 | '=': 'K != "V"', 402 | '!=': 'K == "V"', 403 | '~=': '_WS_K.indexOf(" V ") == -1', 404 | '^=': '!K || K.indexOf("V") != 0', 405 | '*=': '!K || K.indexOf("V") == -1', 406 | '$=': '!K || K.substr(K.length-"V".length) != "V"' 407 | }; 408 | 409 | /**** STATE TRACKER ****/ 410 | 411 | var State = $.selector.State = Base.extend({ 412 | init: function(){ 413 | this.reset(); 414 | }, 415 | reset: function() { 416 | this.attrs = {}; this.wsattrs = {}; 417 | }, 418 | 419 | prev: function(){ 420 | this.reset(); 421 | if (hasElementTraversal) return 'el = el.previousElementSibling'; 422 | return 'while((el = el.previousSibling) && el.nodeType != 1) {}'; 423 | }, 424 | next: function() { 425 | this.reset(); 426 | if (hasElementTraversal) return 'el = el.nextElementSibling'; 427 | return 'while((el = el.nextSibling) && el.nodeType != 1) {}'; 428 | }, 429 | prevLoop: function(body){ 430 | this.reset(); 431 | if (hasElementTraversal) return join([ 'while(el = el.previousElementSibling){', body]); 432 | return join([ 433 | 'while(el = el.previousSibling){', 434 | 'if (el.nodeType != 1) continue;', 435 | body 436 | ]); 437 | }, 438 | parent: function() { 439 | this.reset(); 440 | return 'el = el.parentNode;'; 441 | }, 442 | parentLoop: function(body) { 443 | this.reset(); 444 | return join([ 445 | 'while((el = el.parentNode) && el.nodeType == 1){', 446 | body, 447 | '}' 448 | ]); 449 | }, 450 | 451 | uses_attr: function(attr) { 452 | if (this.attrs[attr]) return; 453 | this.attrs[attr] = true; 454 | return getAttr(attr); 455 | }, 456 | uses_wsattr: function(attr) { 457 | if (this.wsattrs[attr]) return; 458 | this.wsattrs[attr] = true; 459 | return join([this.uses_attr(attr), 'var _WS_'+varForAttr(attr)+' = " "+'+varForAttr(attr)+'+" ";']); 460 | }, 461 | 462 | uses_jqueryFilters: function() { 463 | if (this.jqueryFiltersAdded) return; 464 | this.jqueryFiltersAdded = true; 465 | return 'var _$filters = jQuery.find.selectors.filters;'; 466 | }, 467 | 468 | save: function(lbl) { 469 | return 'var el'+lbl+' = el;'; 470 | }, 471 | restore: function(lbl) { 472 | this.reset(); 473 | return 'el = el'+lbl+';'; 474 | } 475 | }); 476 | 477 | /**** PSEUDO-CLASS DETAILS ****/ 478 | 479 | var pseudoclschecks = { 480 | 'first-child': join([ 481 | 'var cel = el;', 482 | 'while(cel = cel.previousSibling){ if (cel.nodeType === 1) BAD; }' 483 | ]), 484 | 'last-child': join([ 485 | 'var cel = el;', 486 | 'while(cel = cel.nextSibling){ if (cel.nodeType === 1) BAD; }' 487 | ]), 488 | 'nth-child': function(a,b) { 489 | var get_i = join([ 490 | 'var i = 1, cel = el;', 491 | 'while(cel = cel.previousSibling){', 492 | 'if (cel.nodeType === 1) i++;', 493 | '}' 494 | ]); 495 | 496 | if (a == 0) return join([ 497 | get_i, 498 | 'if (i- '+b+' != 0) BAD;' 499 | ]); 500 | else if (b == 0 && a >= 0) return join([ 501 | get_i, 502 | 'if (i%'+a+' != 0 || i/'+a+' < 0) BAD;' 503 | ]); 504 | else if (b == 0 && a < 0) return join([ 505 | 'BAD;' 506 | ]); 507 | else return join([ 508 | get_i, 509 | 'if ((i- '+b+')%'+a+' != 0 || (i- '+b+')/'+a+' < 0) BAD;' 510 | ]); 511 | } 512 | }; 513 | 514 | // Needs to refence contents of object, so must be injected after definition 515 | pseudoclschecks['only-child'] = join([ 516 | pseudoclschecks['first-child'], 517 | pseudoclschecks['last-child'] 518 | ]); 519 | 520 | /**** SimpleSelector ****/ 521 | 522 | $.selector.SimpleSelector.addMethod('compile', function(el) { 523 | var js = []; 524 | 525 | /* Check against element name */ 526 | if (this.tag && this.tag != '*') { 527 | js[js.length] = 'if (el.tagName != "'+this.tag.toUpperCase()+'") BAD;'; 528 | } 529 | 530 | /* Check against ID */ 531 | if (this.id) { 532 | js[js.length] = el.uses_attr('id'); 533 | js[js.length] = 'if (_id !== "'+this.id+'") BAD;'; 534 | } 535 | 536 | /* Build className checking variable */ 537 | if (this.classes.length) { 538 | js[js.length] = el.uses_wsattr('class'); 539 | 540 | /* Check against class names */ 541 | $.each(this.classes, function(i, cls){ 542 | js[js.length] = 'if (_WS__class.indexOf(" '+cls+' ") == -1) BAD;'; 543 | }); 544 | } 545 | 546 | /* Check against attributes */ 547 | $.each(this.attrs, function(i, attr){ 548 | js[js.length] = (attr[1] == '~=') ? el.uses_wsattr(attr[0]) : el.uses_attr(attr[0]); 549 | var check = attrchecks[ attr[1] || '-' ]; 550 | check = check.replace( /K/g, varForAttr(attr[0])).replace( /V/g, attr[2] && attr[2].match(STARTS_WITH_QUOTES) ? attr[2].slice(1,-1) : attr[2] ); 551 | js[js.length] = 'if ('+check+') BAD;'; 552 | }); 553 | 554 | /* Check against nots */ 555 | $.each(this.nots, function(i, not){ 556 | var lbl = ++lbl_id; 557 | var func = join([ 558 | 'l'+lbl+':{', 559 | not.compile(el).replace(BAD, 'break l'+lbl).replace(GOOD, 'BAD'), 560 | '}' 561 | ]); 562 | 563 | if (!(not instanceof $.selector.SimpleSelector)) func = join([ 564 | el.save(lbl), 565 | func, 566 | el.restore(lbl) 567 | ]); 568 | 569 | js[js.length] = func; 570 | }); 571 | 572 | /* Check against pseudo-classes */ 573 | $.each(this.pseudo_classes, function(i, pscls){ 574 | var check = pseudoclschecks[pscls[0]]; 575 | if (check) { 576 | js[js.length] = ( typeof check == 'function' ? check.apply(this, pscls[1]) : check ); 577 | } 578 | else if (check = $.find.selectors.filters[pscls[0]]) { 579 | js[js.length] = el.uses_jqueryFilters(); 580 | js[js.length] = 'if (!_$filters.'+pscls[0]+'(el)) BAD;'; 581 | } 582 | }); 583 | 584 | js[js.length] = 'GOOD'; 585 | 586 | /* Pass */ 587 | return join(js); 588 | }); 589 | 590 | var lbl_id = 0; 591 | /** Turns an compiled fragment into the first part of a combination */ 592 | function as_subexpr(f) { 593 | if (f.complex) 594 | return join([ 595 | 'l'+(++lbl_id)+':{', 596 | f.replace(GOOD, 'break l'+lbl_id), 597 | '}' 598 | ]); 599 | else 600 | return f.replace(GOOD, ''); 601 | } 602 | 603 | var combines = { 604 | ' ': function(el, f1, f2) { 605 | return join_complex([ 606 | f2, 607 | 'while(true){', 608 | el.parent(), 609 | 'if (!el || el.nodeType !== 1) BAD;', 610 | f1.compile(el).replace(BAD, 'continue'), 611 | '}' 612 | ]); 613 | }, 614 | 615 | '>': function(el, f1, f2) { 616 | return join([ 617 | f2, 618 | el.parent(), 619 | 'if (!el || el.nodeType !== 1) BAD;', 620 | f1.compile(el) 621 | ]); 622 | }, 623 | 624 | '~': function(el, f1, f2) { 625 | return join_complex([ 626 | f2, 627 | el.prevLoop(), 628 | f1.compile(el).replace(BAD, 'continue'), 629 | '}', 630 | 'BAD;' 631 | ]); 632 | }, 633 | 634 | '+': function(el, f1, f2) { 635 | return join([ 636 | f2, 637 | el.prev(), 638 | 'if (!el) BAD;', 639 | f1.compile(el) 640 | ]); 641 | } 642 | }; 643 | 644 | $.selector.Selector.addMethod('compile', function(el) { 645 | var l = this.parts.length; 646 | 647 | var expr = this.parts[--l].compile(el); 648 | while (l) { 649 | var combinator = this.parts[--l]; 650 | expr = combines[combinator](el, this.parts[--l], as_subexpr(expr)); 651 | } 652 | 653 | return expr; 654 | }); 655 | 656 | $.selector.SelectorsGroup.addMethod('compile', function(el) { 657 | var expr = [], lbl = ++lbl_id; 658 | 659 | for (var i=0; i < this.parts.length; i++) { 660 | expr[expr.length] = join([ 661 | i == 0 ? el.save(lbl) : el.restore(lbl), 662 | 'l'+lbl+'_'+i+':{', 663 | this.parts[i].compile(el).replace(BAD, 'break l'+lbl+'_'+i), 664 | '}' 665 | ]); 666 | } 667 | 668 | expr[expr.length] = 'BAD;'; 669 | return join(expr); 670 | }); 671 | 672 | $.selector.SelectorBase.addMethod('matches', function(el){ 673 | this.matches = new Function('el', join([ 674 | 'if (!el) return false;', 675 | this.compile(new State()).replace(BAD, 'return false').replace(GOOD, 'return true') 676 | ])); 677 | return this.matches(el); 678 | }); 679 | 680 | })(jQuery); 681 | 682 | ; 683 | 684 | 685 | /* src/jquery.selector.affectedby.js */ 686 | 687 | (function($) { 688 | 689 | // TODO: 690 | // Make attributes & IDs work 691 | 692 | var DIRECT = /DIRECT/g; 693 | var CONTEXT = /CONTEXT/g; 694 | var EITHER = /DIRECT|CONTEXT/g; 695 | 696 | $.selector.SelectorBase.addMethod('affectedBy', function(props) { 697 | this.affectedBy = new Function('props', ([ 698 | 'var direct_classes, context_classes, direct_attrs, context_attrs, t;', 699 | this.ABC_compile().replace(DIRECT, 'direct').replace(CONTEXT, 'context'), 700 | 'return {classes: {context: context_classes, direct: direct_classes}, attrs: {context: context_attrs, direct: direct_attrs}};' 701 | ]).join("\n")); 702 | 703 | // DEBUG: Print out the compiled funciton 704 | // console.log(this.selector, ''+this.affectedBy); 705 | 706 | return this.affectedBy(props); 707 | }); 708 | 709 | $.selector.SimpleSelector.addMethod('ABC_compile', function() { 710 | var parts = []; 711 | 712 | $.each(this.classes, function(i, cls){ 713 | parts[parts.length] = "if (t = props.classes['"+cls+"']) (DIRECT_classes || (DIRECT_classes = {}))['"+cls+"'] = t;"; 714 | }); 715 | 716 | $.each(this.nots, function(i, not){ 717 | parts[parts.length] = not.ABC_compile(); 718 | }); 719 | 720 | return parts.join("\n"); 721 | }); 722 | 723 | $.selector.Selector.addMethod('ABC_compile', function(arg){ 724 | var parts = []; 725 | var i = this.parts.length-1; 726 | 727 | parts[parts.length] = this.parts[i].ABC_compile(); 728 | while ((i = i - 2) >= 0) parts[parts.length] = this.parts[i].ABC_compile().replace(EITHER, 'CONTEXT'); 729 | 730 | return parts.join("\n"); 731 | }); 732 | 733 | $.selector.SelectorsGroup.addMethod('ABC_compile', function(){ 734 | var parts = []; 735 | 736 | $.each(this.parts, function(i,part){ 737 | parts[parts.length] = part.ABC_compile(); 738 | }); 739 | 740 | return parts.join("\n"); 741 | }); 742 | 743 | 744 | })(jQuery); 745 | ; 746 | 747 | 748 | /* src/jquery.focusinout.js */ 749 | 750 | (function($){ 751 | 752 | /** 753 | * Add focusin and focusout support to bind and live for browers other than IE. Designed to be usable in a delegated fashion (like $.live) 754 | * Copyright (c) 2007 Jörn Zaefferer 755 | */ 756 | if ($.support.focusinBubbles === undefined) { 757 | $.support.focusinBubbles = !!($.browser) && !$.browser.msie; 758 | } 759 | 760 | if (!$.support.focusinBubbles && !$.event.special.focusin) { 761 | // Emulate focusin and focusout by binding focus and blur in capturing mode 762 | $.each({focus: 'focusin', blur: 'focusout'}, function(original, fix){ 763 | $.event.special[fix] = { 764 | setup: function(){ 765 | if (!this.addEventListener) return false; 766 | this.addEventListener(original, $.event.special[fix].handler, true); 767 | }, 768 | teardown: function(){ 769 | if (!this.removeEventListener) return false; 770 | this.removeEventListener(original, $.event.special[fix].handler, true); 771 | }, 772 | handler: function(e){ 773 | arguments[0] = $.event.fix(e); 774 | arguments[0].type = fix; 775 | return $.event.handle.apply(this, arguments); 776 | } 777 | }; 778 | }); 779 | } 780 | 781 | (function(){ 782 | //IE has some trouble with focusout with select and keyboard navigation 783 | var activeFocus = null; 784 | 785 | $(document) 786 | .bind('focusin', function(e){ 787 | var target = e.realTarget || e.target; 788 | if (activeFocus && activeFocus !== target) { 789 | e.type = 'focusout'; 790 | $(activeFocus).trigger(e); 791 | e.type = 'focusin'; 792 | e.target = target; 793 | } 794 | activeFocus = target; 795 | }) 796 | .bind('focusout', function(e){ 797 | activeFocus = null; 798 | }); 799 | })(); 800 | 801 | })(jQuery); 802 | ; 803 | 804 | 805 | /* src/jquery.entwine.js */ 806 | 807 | try { 808 | console.log; 809 | } 810 | catch (e) { 811 | window.console = undefined; 812 | } 813 | 814 | (function($) { 815 | 816 | /* Create a subclass of the jQuery object. This was introduced in jQuery 1.5, but removed again in 1.9 */ 817 | var sub = function() { 818 | function jQuerySub( selector, context ) { 819 | return new jQuerySub.fn.init( selector, context ); 820 | } 821 | 822 | jQuery.extend( true, jQuerySub, $ ); 823 | jQuerySub.superclass = $; 824 | jQuerySub.fn = jQuerySub.prototype = $(); 825 | jQuerySub.fn.constructor = jQuerySub; 826 | jQuerySub.fn.init = function init( selector, context ) { 827 | var instance = jQuery.fn.init.call( this, selector, context, rootjQuerySub ); 828 | return instance instanceof jQuerySub ? 829 | instance : 830 | jQuerySub( instance ); 831 | }; 832 | jQuerySub.fn.init.prototype = jQuerySub.fn; 833 | var rootjQuerySub = jQuerySub(document); 834 | return jQuerySub; 835 | }; 836 | 837 | var namespaces = {}; 838 | 839 | $.entwine = function() { 840 | $.fn.entwine.apply(null, arguments); 841 | }; 842 | 843 | /** 844 | * A couple of utility functions for accessing the store outside of this closure, and for making things 845 | * operate in a little more easy-to-test manner 846 | */ 847 | $.extend($.entwine, { 848 | /** 849 | * Get all the namespaces. Useful for introspection? Internal interface of Namespace not guaranteed consistant 850 | */ 851 | namespaces: namespaces, 852 | 853 | /** 854 | * Remove all entwine rules 855 | */ 856 | clear_all_rules: function() { 857 | // Remove proxy functions 858 | for (var k in $.fn) { if ($.fn[k].isentwinemethod) delete $.fn[k]; } 859 | // Remove bound events - TODO: Make this pluggable, so this code can be moved to jquery.entwine.events.js 860 | $(document).unbind('.entwine'); 861 | $(window).unbind('.entwine'); 862 | // Remove namespaces, and start over again 863 | for (var k in namespaces) delete namespaces[k]; 864 | for (var k in $.entwine.capture_bindings) delete $.entwine.capture_bindings[k]; 865 | }, 866 | 867 | WARN_LEVEL_NONE: 0, 868 | WARN_LEVEL_IMPORTANT: 1, 869 | WARN_LEVEL_BESTPRACTISE: 2, 870 | 871 | /** 872 | * Warning level. Set to a higher level to get warnings dumped to console. 873 | */ 874 | warningLevel: 0, 875 | 876 | /** Utility to optionally display warning messages depending on level */ 877 | warn: function(message, level) { 878 | if (level <= $.entwine.warningLevel && console && console.warn) { 879 | console.warn(message); 880 | if (console.trace) console.trace(); 881 | } 882 | }, 883 | 884 | warn_exception: function(where, /* optional: */ on, e) { 885 | if ($.entwine.WARN_LEVEL_IMPORTANT <= $.entwine.warningLevel && console && console.warn) { 886 | if (arguments.length == 2) { e = on; on = null; } 887 | 888 | if (on) console.warn('Uncaught exception',e,'in',where,'on',on); 889 | else console.warn('Uncaught exception',e,'in',where); 890 | 891 | if (e.stack) console.warn("Stack Trace:\n" + e.stack); 892 | } 893 | } 894 | }); 895 | 896 | 897 | /** Stores a count of definitions, so that we can sort identical selectors by definition order */ 898 | var rulecount = 0; 899 | 900 | var Rule = Base.extend({ 901 | init: function(selector, name) { 902 | this.selector = selector; 903 | this.specifity = selector.specifity(); 904 | this.important = 0; 905 | this.name = name; 906 | this.rulecount = rulecount++; 907 | } 908 | }); 909 | 910 | Rule.compare = function(a, b) { 911 | var as = a.specifity, bs = b.specifity; 912 | 913 | return (a.important - b.important) || 914 | (as[0] - bs[0]) || 915 | (as[1] - bs[1]) || 916 | (as[2] - bs[2]) || 917 | (a.rulecount - b.rulecount) ; 918 | }; 919 | 920 | $.entwine.RuleList = function() { 921 | var list = []; 922 | 923 | list.addRule = function(selector, name){ 924 | var rule = Rule(selector, name); 925 | 926 | list[list.length] = rule; 927 | list.sort(Rule.compare); 928 | 929 | return rule; 930 | }; 931 | 932 | return list; 933 | }; 934 | 935 | var handlers = []; 936 | 937 | /** 938 | * A Namespace holds all the information needed for adding entwine methods to a namespace (including the _null_ namespace) 939 | */ 940 | $.entwine.Namespace = Base.extend({ 941 | init: function(name){ 942 | if (name && !name.match(/^[A-Za-z0-9.]+$/)) $.entwine.warn('Entwine namespace '+name+' is not formatted as period seperated identifiers', $.entwine.WARN_LEVEL_BESTPRACTISE); 943 | name = name || '__base'; 944 | 945 | this.name = name; 946 | this.store = {}; 947 | 948 | namespaces[name] = this; 949 | 950 | if (name == "__base") { 951 | this.injectee = $.fn; 952 | this.$ = $; 953 | } 954 | else { 955 | // We're in a namespace, so we build a Class that subclasses the jQuery Object Class to inject namespace functions into 956 | this.$ = $.sub ? $.sub() : sub(); 957 | // Work around bug in sub() - subclass must share cache with root or data won't get cleared by cleanData 958 | this.$.cache = $.cache; 959 | 960 | this.injectee = this.$.prototype; 961 | 962 | // We override entwine to inject the name of this namespace when defining blocks inside this namespace 963 | var entwine_wrapper = this.injectee.entwine = function(spacename) { 964 | var args = arguments; 965 | 966 | if (!spacename || typeof spacename != 'string') { args = $.makeArray(args); args.unshift(name); } 967 | else if (spacename.charAt(0) != '.') args[0] = name+'.'+spacename; 968 | 969 | return $.fn.entwine.apply(this, args); 970 | }; 971 | 972 | this.$.entwine = function() { 973 | entwine_wrapper.apply(null, arguments); 974 | }; 975 | 976 | for (var i = 0; i < handlers.length; i++) { 977 | var handler = handlers[i], builder; 978 | 979 | // Inject jQuery object method overrides 980 | if (builder = handler.namespaceMethodOverrides) { 981 | var overrides = builder(this); 982 | for (var k in overrides) this.injectee[k] = overrides[k]; 983 | } 984 | 985 | // Inject $.entwine function overrides 986 | if (builder = handler.namespaceStaticOverrides) { 987 | var overrides = builder(this); 988 | for (var k in overrides) this.$.entwine[k] = overrides[k]; 989 | } 990 | } 991 | } 992 | }, 993 | 994 | /** 995 | * Returns a function that does selector matching against the function list for a function name 996 | * Used by proxy for all calls, and by ctorProxy to handle _super calls 997 | * @param {String} name - name of the function as passed in the construction object 998 | * @param {String} funcprop - the property on the Rule object that gives the actual function to call 999 | * @param {function} basefunc - the non-entwine function to use as the catch-all function at the bottom of the stack 1000 | */ 1001 | one: function(name, funcprop, basefunc) { 1002 | var namespace = this; 1003 | var funcs = this.store[name]; 1004 | 1005 | var one = function(el, args, i){ 1006 | if (i === undefined) i = funcs.length; 1007 | while (i--) { 1008 | if (funcs[i].selector.matches(el)) { 1009 | var ret, tmp_i = el.i, tmp_f = el.f; 1010 | el.i = i; el.f = one; 1011 | try { ret = funcs[i][funcprop].apply(namespace.$(el), args); } 1012 | finally { el.i = tmp_i; el.f = tmp_f; } 1013 | return ret; 1014 | } 1015 | } 1016 | // If we didn't find a entwine-defined function, but there is a non-entwine function to use as a base, try that 1017 | if (basefunc) return basefunc.apply(namespace.$(el), args); 1018 | }; 1019 | 1020 | return one; 1021 | }, 1022 | 1023 | /** 1024 | * A proxy is a function attached to a callable object (either the base jQuery.fn or a subspace object) which handles 1025 | * finding and calling the correct function for each member of the current jQuery context 1026 | * @param {String} name - name of the function as passed in the construction object 1027 | * @param {function} basefunc - the non-entwine function to use as the catch-all function at the bottom of the stack 1028 | */ 1029 | build_proxy: function(name, basefunc) { 1030 | var one = this.one(name, 'func', basefunc); 1031 | 1032 | var prxy = function() { 1033 | var rv, ctx = $(this); 1034 | 1035 | var i = ctx.length; 1036 | while (i--) rv = one(ctx[i], arguments); 1037 | return rv; 1038 | }; 1039 | 1040 | return prxy; 1041 | }, 1042 | 1043 | bind_proxy: function(selector, name, func) { 1044 | var rulelist = this.store[name] || (this.store[name] = $.entwine.RuleList()); 1045 | 1046 | var rule = rulelist.addRule(selector, name); rule.func = func; 1047 | 1048 | if (!this.injectee.hasOwnProperty(name) || !this.injectee[name].isentwinemethod) { 1049 | this.injectee[name] = this.build_proxy(name, this.injectee.hasOwnProperty(name) ? this.injectee[name] : null); 1050 | this.injectee[name].isentwinemethod = true; 1051 | } 1052 | 1053 | if (!this.injectee[name].isentwinemethod) { 1054 | $.entwine.warn('Warning: Entwine function '+name+' clashes with regular jQuery function - entwine function will not be callable directly on jQuery object', $.entwine.WARN_LEVEL_IMPORTANT); 1055 | } 1056 | }, 1057 | 1058 | add: function(selector, data) { 1059 | // For every item in the hash, try ever method handler, until one returns true 1060 | for (var k in data) { 1061 | var v = data[k]; 1062 | 1063 | for (var i = 0; i < handlers.length; i++) { 1064 | if (handlers[i].bind && handlers[i].bind.call(this, selector, k, v)) break; 1065 | } 1066 | } 1067 | }, 1068 | 1069 | has: function(ctx, name) { 1070 | var rulelist = this.store[name]; 1071 | if (!rulelist) return false; 1072 | 1073 | /* We go forward this time, since low specifity is likely to knock out a bunch of elements quickly */ 1074 | for (var i = 0 ; i < rulelist.length; i++) { 1075 | ctx = ctx.not(rulelist[i].selector); 1076 | if (!ctx.length) return true; 1077 | } 1078 | return false; 1079 | } 1080 | }); 1081 | 1082 | /** 1083 | * A handler is some javascript code that adds support for some time of key / value pair passed in the hash to the Namespace add method. 1084 | * The default handlers provided (and included by default) are event, ctor and properties 1085 | */ 1086 | $.entwine.Namespace.addHandler = function(handler) { 1087 | for (var i = 0; i < handlers.length && handlers[i].order < handler.order; i++) { /* Pass */ } 1088 | handlers.splice(i, 0, handler); 1089 | }; 1090 | 1091 | $.entwine.Namespace.addHandler({ 1092 | order: 50, 1093 | 1094 | bind: function(selector, k, v){ 1095 | if ($.isFunction(v)) { 1096 | this.bind_proxy(selector, k, v); 1097 | return true; 1098 | } 1099 | } 1100 | }); 1101 | 1102 | $.extend($.fn, { 1103 | /** 1104 | * Main entwine function. Used for new definitions, calling into a namespace (or forcing the base namespace) and entering a using block 1105 | * 1106 | */ 1107 | entwine: function(spacename) { 1108 | var i = 0; 1109 | /* Don't actually work out selector until we try and define something on it - we might be opening a namespace on an function-traveresed object 1110 | which have non-standard selectors like .parents(.foo).slice(0,1) */ 1111 | var selector = null; 1112 | 1113 | /* By default we operator on the base namespace */ 1114 | var namespace = namespaces.__base || $.entwine.Namespace(); 1115 | 1116 | /* If the first argument is a string, then it's the name of a namespace. Look it up */ 1117 | if (typeof spacename == 'string') { 1118 | if (spacename.charAt('0') == '.') spacename = spacename.substr(1); 1119 | if (spacename) namespace = namespaces[spacename] || $.entwine.Namespace(spacename); 1120 | i=1; 1121 | } 1122 | 1123 | /* All remaining arguments should either be using blocks or definition hashs */ 1124 | while (i < arguments.length) { 1125 | var res = arguments[i++]; 1126 | 1127 | // If it's a function, call it - either it's a using block or it's a namespaced entwine definition 1128 | if ($.isFunction(res)) { 1129 | if (res.length != 1) $.entwine.warn('Function block inside entwine definition does not take $ argument properly', $.entwine.WARN_LEVEL_IMPORTANT); 1130 | res = res.call(namespace.$(this), namespace.$); 1131 | } 1132 | 1133 | // If we have a entwine definition hash, inject it into namespace 1134 | if (res) { 1135 | if (selector === null) selector = this.selector ? $.selector(this.selector) : false; 1136 | 1137 | if (selector) namespace.add(selector, res); 1138 | else $.entwine.warn('Entwine block given to entwine call without selector. Make sure you call $(selector).entwine when defining blocks', $.entwine.WARN_LEVEL_IMPORTANT); 1139 | } 1140 | } 1141 | 1142 | /* Finally, return the jQuery object 'this' refers to, wrapped in the new namespace */ 1143 | return namespace.$(this); 1144 | }, 1145 | 1146 | /** 1147 | * Calls the next most specific version of the current entwine method 1148 | */ 1149 | _super: function(){ 1150 | var rv, i = this.length; 1151 | while (i--) { 1152 | var el = this[0]; 1153 | rv = el.f(el, arguments, el.i); 1154 | } 1155 | return rv; 1156 | } 1157 | }); 1158 | 1159 | })(jQuery); 1160 | ; 1161 | 1162 | 1163 | /* src/domevents/jquery.entwine.domevents.addrem.js */ 1164 | 1165 | (function($){ 1166 | 1167 | // Gets all the child elements of a particular elements, stores it in an array 1168 | function getElements(store, original) { 1169 | var node, i = store.length, next = (typeof original.firstChild == 'undefined' ? $(original)[0].firstChild : original); 1170 | 1171 | while ((node = next)) { 1172 | if (node.nodeType === 1) store[i++] = node; 1173 | next = node.firstChild || node.nextSibling; 1174 | while (!next && (node = node.parentNode) && node !== original) next = node.nextSibling; 1175 | } 1176 | } 1177 | 1178 | // This might be faster? Or slower? @todo: benchmark. 1179 | function getElementsAlt(store, node) { 1180 | if (node.getElementsByTagName) { 1181 | var els = node.getElementsByTagName('*'), len = els.length, i = 0, j = store.length; 1182 | for(; i < len; i++, j++) { 1183 | store[j] = els[i]; 1184 | } 1185 | } 1186 | else if (node.childNodes) { 1187 | var els = node.childNodes, len = els.length, i = 0; 1188 | for(; i < len; i++) { 1189 | getElements(store, els[i]); 1190 | } 1191 | } 1192 | } 1193 | 1194 | var isArrayLike = function(obj){ 1195 | // The `in` check is from jQuery’s fix for an iOS 8 64-bit JIT object length bug: 1196 | // https://github.com/jquery/jquery/pull/2185 1197 | // When passing a non-object (e.g. boolean) can.each fails where it previously did nothing. 1198 | // https://github.com/canjs/canjs/issues/1989 1199 | var length = obj && typeof obj !== 'boolean' && 1200 | typeof obj !== 'number' && 1201 | "length" in obj && obj.length; 1202 | 1203 | // var length = "length" in obj && obj.length; 1204 | return typeof arr !== "function" && 1205 | ( length === 0 || typeof length === "number" && length > 0 && ( length - 1 ) in obj ); 1206 | }; 1207 | 1208 | var dontTrigger = false; 1209 | 1210 | var version = $.prototype.jquery.split('.'); 1211 | 1212 | // @TODO Modified by Ed to make compatible with jQuery 3.6.0 1213 | // Monkey patch dom manipulation to catch all regular jQuery add element calls 1214 | ['after', 'prepend', 'before', 'append','replaceWith'].forEach(function (name) { 1215 | var original = $.fn[name]; 1216 | $.fn[name] = function (elem) { 1217 | var added = []; 1218 | 1219 | if (!dontTrigger) { 1220 | // @TODO Modified by Ed to make compatible with jQuery 3.6.0 1221 | if (elem.each) { 1222 | elem.each(function (key, _elem) { 1223 | if (_elem.nodeType == 1) added[added.length] = _elem; 1224 | getElements(added, _elem); 1225 | }); 1226 | } else { 1227 | if (elem.nodeType == 1) added[added.length] = elem; 1228 | getElements(added, elem); 1229 | } 1230 | } 1231 | 1232 | var rv = original.apply(this, arguments); 1233 | 1234 | if (!dontTrigger && added.length) { 1235 | var event = $.Event('EntwineElementsAdded'); 1236 | event.targets = added; 1237 | $(document).triggerHandler(event); 1238 | } 1239 | 1240 | return rv; 1241 | }; 1242 | }); 1243 | 1244 | // Monkey patch $.fn.html to catch when jQuery sets innerHTML directly 1245 | var _html = $.prototype.html; 1246 | $.prototype.html = function(value) { 1247 | if (value === undefined) return _html.apply(this, arguments); 1248 | 1249 | dontTrigger = true; 1250 | var res = _html.apply(this, arguments); 1251 | dontTrigger = false; 1252 | 1253 | var added = []; 1254 | 1255 | var i = 0, length = this.length; 1256 | for (; i < length; i++ ) getElements(added, this[i]); 1257 | 1258 | var event = $.Event('EntwineElementsAdded'); 1259 | event.targets = added; 1260 | $(document).triggerHandler(event); 1261 | 1262 | return res; 1263 | } 1264 | 1265 | // If this is true, we've changed something to call cleanData so that we can catch the elements, but we don't 1266 | // want to call the underlying original $.cleanData 1267 | var supressActualClean = false; 1268 | 1269 | // Monkey patch $.cleanData to catch element removal 1270 | var _cleanData = $.cleanData; 1271 | $.cleanData = function( elems ) { 1272 | // By default we can assume all elements passed are legitimately being removeed 1273 | var removed = elems; 1274 | 1275 | // Except if we're supressing actual clean - we might be being called by jQuery "being careful" about detaching nodes 1276 | // before attaching them. So we need to check to make sure these nodes currently are in a document 1277 | if (supressActualClean) { 1278 | var i = 0, len = elems.length, removed = [], ri = 0; 1279 | for(; i < len; i++) { 1280 | var node = elems[i], current = node; 1281 | while (current = current.parentNode) { 1282 | if (current.nodeType == 9) { removed[ri++] = node; break; } 1283 | } 1284 | } 1285 | } 1286 | 1287 | if (removed.length) { 1288 | var event = $.Event('EntwineElementsRemoved'); 1289 | event.targets = removed; 1290 | $(document).triggerHandler(event); 1291 | } 1292 | 1293 | if (!supressActualClean) _cleanData.apply(this, arguments); 1294 | } 1295 | 1296 | // Monkey patch $.fn.remove to catch when we're just detaching (keepdata == 1) - 1297 | // this doesn't call cleanData but still needs to trigger event 1298 | var _remove = $.prototype.remove; 1299 | $.prototype.remove = function(selector, keepdata) { 1300 | supressActualClean = keepdata; 1301 | var rv = _remove.call(this, selector); 1302 | supressActualClean = false; 1303 | return rv; 1304 | } 1305 | 1306 | // And on DOM ready, trigger adding once 1307 | $(function(){ 1308 | var added = []; getElements(added, document); 1309 | 1310 | var event = $.Event('EntwineElementsAdded'); 1311 | event.targets = added; 1312 | $(document).triggerHandler(event); 1313 | }); 1314 | 1315 | 1316 | })(jQuery);; 1317 | 1318 | 1319 | /* src/domevents/jquery.entwine.domevents.maybechanged.js */ 1320 | 1321 | (function($){ 1322 | 1323 | /** Utility function to monkey-patch a jQuery method */ 1324 | var monkey = function( /* method, method, ...., patch */){ 1325 | var methods = $.makeArray(arguments); 1326 | var patch = methods.pop(); 1327 | 1328 | $.each(methods, function(i, method){ 1329 | var old = $.fn[method]; 1330 | 1331 | $.fn[method] = function() { 1332 | var self = this, args = $.makeArray(arguments); 1333 | 1334 | var rv = old.apply(self, args); 1335 | patch.apply(self, args); 1336 | return rv; 1337 | } 1338 | }); 1339 | } 1340 | 1341 | /** What to call to run a function 'soon'. Normally setTimeout, but for syncronous mode we override so soon === now */ 1342 | var runSoon = window.setTimeout; 1343 | 1344 | /** The timer handle for the asyncronous matching call */ 1345 | var ChangeDetails = Base.extend({ 1346 | 1347 | init: function() { 1348 | this.global = false; 1349 | this.attrs = {}; 1350 | this.classes = {}; 1351 | }, 1352 | 1353 | /** Fire the change event. Only fires on the document node, so bind to that */ 1354 | triggerEvent: function() { 1355 | // If we're not the active changes instance any more, don't trigger 1356 | if (changes != this) return; 1357 | 1358 | // Cancel any pending timeout (if we're directly called in the mean time) 1359 | if (this.check_id) clearTimeout(this.check_id); 1360 | 1361 | // Reset the global changes object to be a new instance (do before trigger, in case trigger fires changes itself) 1362 | changes = new ChangeDetails(); 1363 | 1364 | // Fire event 1365 | $(document).triggerHandler("EntwineSubtreeMaybeChanged", [this]); 1366 | }, 1367 | 1368 | changed: function() { 1369 | if (!this.check_id) { 1370 | var self = this; 1371 | this.check_id = runSoon(function(){ self.check_id = null; self.triggerEvent(); }, 10); 1372 | } 1373 | }, 1374 | 1375 | addAll: function() { 1376 | if (this.global) return this; // If we've already flagged as a global change, just skip 1377 | 1378 | this.global = true; 1379 | this.changed(); 1380 | return this; 1381 | }, 1382 | 1383 | addSubtree: function(node) { 1384 | return this.addAll(); 1385 | }, 1386 | 1387 | /* For now we don't do this. It's expensive, and jquery.entwine.ctors doesn't use this information anyway */ 1388 | addSubtreeFuture: function(node) { 1389 | if (this.global) return this; // If we've already flagged as a global change, just skip 1390 | 1391 | this.subtree = this.subtree ? this.subtree.add(node) : $(node); 1392 | this.changed(); 1393 | return this; 1394 | }, 1395 | 1396 | addAttr: function(attr, node) { 1397 | if (this.global) return this; 1398 | 1399 | this.attrs[attr] = (attr in this.attrs) ? this.attrs[attr].add(node) : $(node); 1400 | this.changed(); 1401 | return this; 1402 | }, 1403 | 1404 | addClass: function(klass, node) { 1405 | if (this.global) return this; 1406 | 1407 | this.classes[klass] = (klass in this.classes) ? this.classes[klass].add(node) : $(node); 1408 | this.changed(); 1409 | return this; 1410 | } 1411 | }); 1412 | 1413 | var changes = new ChangeDetails(); 1414 | 1415 | // Element add events trigger maybechanged events 1416 | 1417 | $(document).bind('EntwineElementsAdded', function(e){ changes.addSubtree(e.targets); }); 1418 | 1419 | // Element remove events trigger maybechanged events, but we have to wait until after the nodes are actually removed 1420 | // (EntwineElementsRemoved fires _just before_ the elements are removed so the data still exists), especially in syncronous mode 1421 | 1422 | var removed = null; 1423 | $(document).bind('EntwineElementsRemoved', function(e){ removed = e.targets; }); 1424 | 1425 | monkey('remove', 'html', 'empty', function(){ 1426 | var subtree = removed; removed = null; 1427 | if (subtree) changes.addSubtree(subtree); 1428 | }); 1429 | 1430 | // We also need to know when an attribute, class, etc changes. Patch the relevant jQuery methods here 1431 | 1432 | monkey('removeAttr', function(attr){ 1433 | changes.addAttr(attr, this); 1434 | }); 1435 | 1436 | monkey('addClass', 'removeClass', 'toggleClass', function(klass){ 1437 | if (typeof klass == 'string') changes.addClass(klass, this); 1438 | }); 1439 | 1440 | monkey('attr', function(a, b){ 1441 | if (b !== undefined && typeof a == 'string') changes.addAttr(a, this); 1442 | else if (typeof a != 'string') { for (var k in a) changes.addAttr(k, this); } 1443 | }); 1444 | 1445 | // Add some usefull accessors to $.entwine 1446 | 1447 | $.extend($.entwine, { 1448 | /** 1449 | * Make onmatch and onunmatch work in synchronous mode - that is, new elements will be detected immediately after 1450 | * the DOM manipulation that made them match. This is only really useful for during testing, since it's pretty slow 1451 | * (otherwise we'd make it the default). 1452 | */ 1453 | synchronous_mode: function() { 1454 | if (changes && changes.check_id) clearTimeout(changes.check_id); 1455 | changes = new ChangeDetails(); 1456 | 1457 | runSoon = function(func, delay){ func.call(this); return null; }; 1458 | }, 1459 | 1460 | /** 1461 | * Trigger onmatch and onunmatch now - usefull for after DOM manipulation by methods other than through jQuery. 1462 | * Called automatically on document.ready 1463 | */ 1464 | triggerMatching: function() { 1465 | changes.addAll(); 1466 | } 1467 | }); 1468 | 1469 | })(jQuery);; 1470 | 1471 | 1472 | /* src/jquery.entwine.events.js */ 1473 | 1474 | (function($) { 1475 | 1476 | /** Taken from jQuery 1.5.2 for backwards compatibility */ 1477 | if ($.support.changeBubbles == undefined) { 1478 | $.support.changeBubbles = true; 1479 | 1480 | var el = document.createElement("div"); 1481 | eventName = "onchange"; 1482 | 1483 | if (el.attachEvent) { 1484 | var isSupported = (eventName in el); 1485 | if (!isSupported) { 1486 | el.setAttribute(eventName, "return;"); 1487 | isSupported = typeof el[eventName] === "function"; 1488 | } 1489 | 1490 | $.support.changeBubbles = isSupported; 1491 | } 1492 | } 1493 | 1494 | /* Return true if node b is the same as, or is a descendant of, node a */ 1495 | if (document.compareDocumentPosition) { 1496 | var is_or_contains = function(a, b) { 1497 | return a && b && (a == b || !!(a.compareDocumentPosition(b) & 16)); 1498 | }; 1499 | } 1500 | else { 1501 | var is_or_contains = function(a, b) { 1502 | return a && b && (a == b || (a.contains ? a.contains(b) : true)); 1503 | }; 1504 | } 1505 | 1506 | /* Add the methods to handle event binding to the Namespace class */ 1507 | $.entwine.Namespace.addMethods({ 1508 | build_event_proxy: function(name) { 1509 | var one = this.one(name, 'func'); 1510 | 1511 | var prxy = function(e, data) { 1512 | // For events that do not bubble we manually trigger delegation (see delegate_submit below) 1513 | // If this event is a manual trigger, the event we actually want to bubble is attached as a property of the passed event 1514 | e = e.delegatedEvent || e; 1515 | 1516 | var el = e.target; 1517 | while (el && el.nodeType == 1 && !e.isPropagationStopped()) { 1518 | var ret = one(el, arguments); 1519 | if (ret !== undefined) e.result = ret; 1520 | if (ret === false) { e.preventDefault(); e.stopPropagation(); } 1521 | 1522 | el = el.parentNode; 1523 | } 1524 | }; 1525 | 1526 | return prxy; 1527 | }, 1528 | 1529 | build_mouseenterleave_proxy: function(name) { 1530 | var one = this.one(name, 'func'); 1531 | 1532 | var prxy = function(e) { 1533 | var el = e.target; 1534 | var rel = e.relatedTarget; 1535 | 1536 | while (el && el.nodeType == 1 && !e.isPropagationStopped()) { 1537 | /* We know el contained target. If it also contains relatedTarget then we didn't mouseenter / leave. What's more, every ancestor will also 1538 | contan el and rel, and so we can just stop bubbling */ 1539 | if (is_or_contains(el, rel)) break; 1540 | 1541 | var ret = one(el, arguments); 1542 | if (ret !== undefined) e.result = ret; 1543 | if (ret === false) { e.preventDefault(); e.stopPropagation(); } 1544 | 1545 | el = el.parentNode; 1546 | } 1547 | }; 1548 | 1549 | return prxy; 1550 | }, 1551 | 1552 | build_change_proxy: function(name) { 1553 | var one = this.one(name, 'func'); 1554 | 1555 | /* 1556 | This change bubble emulation code is taken mostly from jQuery 1.6 - unfortunately we can't easily reuse any of 1557 | it without duplication, so we'll have to re-migrate any bugfixes 1558 | */ 1559 | 1560 | // Get the value of an item. Isn't supposed to be interpretable, just stable for some value, and different 1561 | // once the value changes 1562 | var getVal = function( elem ) { 1563 | var type = elem.type, val = elem.value; 1564 | 1565 | if (type === "radio" || type === "checkbox") { 1566 | val = elem.checked; 1567 | } 1568 | else if (type === "select-multiple") { 1569 | val = ""; 1570 | if (elem.selectedIndex > -1) { 1571 | val = jQuery.map(elem.options, function(elem){ return elem.selected; }).join("-"); 1572 | } 1573 | } 1574 | else if (jQuery.nodeName(elem, "select")) { 1575 | val = elem.selectedIndex; 1576 | } 1577 | 1578 | return val; 1579 | }; 1580 | 1581 | // Test if a node name is a form input 1582 | var rformElems = /^(?:textarea|input|select)$/i; 1583 | 1584 | // Check if this event is a change, and bubble the change event if it is 1585 | var testChange = function(e) { 1586 | var elem = e.target, data, val; 1587 | 1588 | if (!rformElems.test(elem.nodeName) || elem.readOnly) return; 1589 | 1590 | data = jQuery.data(elem, "_entwine_change_data"); 1591 | val = getVal(elem); 1592 | 1593 | // the current data will be also retrieved by beforeactivate 1594 | if (e.type !== "focusout" || elem.type !== "radio") { 1595 | jQuery.data(elem, "_entwine_change_data", val); 1596 | } 1597 | 1598 | if (data === undefined || val === data) return; 1599 | 1600 | if (data != null || val) { 1601 | e.type = "change"; 1602 | 1603 | while (elem && elem.nodeType == 1 && !e.isPropagationStopped()) { 1604 | var ret = one(elem, arguments); 1605 | if (ret !== undefined) e.result = ret; 1606 | if (ret === false) { e.preventDefault(); e.stopPropagation(); } 1607 | 1608 | elem = elem.parentNode; 1609 | } 1610 | } 1611 | }; 1612 | 1613 | // The actual proxy - responds to several events, some of which triger a change check, some 1614 | // of which just store the value for future change checks 1615 | var prxy = function(e) { 1616 | var event = e.type, elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : ""; 1617 | 1618 | switch (event) { 1619 | case 'focusout': 1620 | case 'beforedeactivate': 1621 | testChange.apply(this, arguments); 1622 | break; 1623 | 1624 | case 'click': 1625 | if ( type === "radio" || type === "checkbox" || jQuery.nodeName( elem, "select" ) ) { 1626 | testChange.apply(this, arguments); 1627 | } 1628 | break; 1629 | 1630 | // Change has to be called before submit 1631 | // Keydown will be called before keypress, which is used in submit-event delegation 1632 | case 'keydown': 1633 | if ( 1634 | (e.keyCode === 13 && !jQuery.nodeName( elem, "textarea" ) ) || 1635 | (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || 1636 | type === "select-multiple" 1637 | ) { 1638 | testChange.apply(this, arguments); 1639 | } 1640 | break; 1641 | 1642 | // Beforeactivate happens also before the previous element is blurred 1643 | // with this event you can't trigger a change event, but you can store 1644 | // information 1645 | case 'focusin': 1646 | case 'beforeactivate': 1647 | jQuery.data( elem, "_entwine_change_data", getVal(elem) ); 1648 | break; 1649 | } 1650 | } 1651 | 1652 | return prxy; 1653 | }, 1654 | 1655 | bind_event: function(selector, name, func, event) { 1656 | var funcs = this.store[name] || (this.store[name] = $.entwine.RuleList()) ; 1657 | var proxies = funcs.proxies || (funcs.proxies = {}); 1658 | 1659 | var rule = funcs.addRule(selector, name); rule.func = func; 1660 | 1661 | if (!proxies[name]) { 1662 | switch (name) { 1663 | case 'onmouseenter': 1664 | proxies[name] = this.build_mouseenterleave_proxy(name); 1665 | event = 'mouseover'; 1666 | break; 1667 | case 'onmouseleave': 1668 | proxies[name] = this.build_mouseenterleave_proxy(name); 1669 | event = 'mouseout'; 1670 | break; 1671 | case 'onchange': 1672 | if (!$.support.changeBubbles) { 1673 | proxies[name] = this.build_change_proxy(name); 1674 | event = 'click keydown focusin focusout beforeactivate beforedeactivate'; 1675 | } 1676 | break; 1677 | case 'onsubmit': 1678 | event = 'delegatedSubmit'; 1679 | break; 1680 | case 'onfocus': 1681 | case 'onblur': 1682 | $.entwine.warn('Event '+event+' not supported - using focusin / focusout instead', $.entwine.WARN_LEVEL_IMPORTANT); 1683 | } 1684 | 1685 | // If none of the special handlers created a proxy, use the generic proxy 1686 | if (!proxies[name]) proxies[name] = this.build_event_proxy(name); 1687 | 1688 | $(document).bind(event.replace(/(\s+|$)/g, '.entwine$1'), proxies[name]); 1689 | } 1690 | } 1691 | }); 1692 | 1693 | $.entwine.Namespace.addHandler({ 1694 | order: 40, 1695 | 1696 | bind: function(selector, k, v){ 1697 | var match, event; 1698 | if ($.isFunction(v) && (match = k.match(/^on(.*)/))) { 1699 | event = match[1]; 1700 | this.bind_event(selector, k, v, event); 1701 | return true; 1702 | } 1703 | } 1704 | }); 1705 | 1706 | // Find all forms and bind onsubmit to trigger on the document too. 1707 | // This is the only event that can't be grabbed via delegation 1708 | 1709 | var delegate_submit = function(e, data){ 1710 | var delegationEvent = $.Event('delegatedSubmit'); delegationEvent.delegatedEvent = e; 1711 | return $(document).trigger(delegationEvent, data); 1712 | }; 1713 | 1714 | $(document).bind('EntwineElementsAdded', function(e){ 1715 | var forms = $(e.targets).filter('form'); 1716 | if (!forms.length) return; 1717 | 1718 | forms.bind('submit.entwine_delegate_submit', delegate_submit); 1719 | }); 1720 | 1721 | })(jQuery); 1722 | ; 1723 | 1724 | 1725 | /* src/jquery.entwine.eventcapture.js */ 1726 | 1727 | (function($) { 1728 | 1729 | $.entwine.Namespace.addMethods({ 1730 | bind_capture: function(selector, event, name, capture) { 1731 | var store = this.captures || (this.captures = {}); 1732 | var rulelists = store[event] || (store[event] = {}); 1733 | var rulelist = rulelists[name] || (rulelists[name] = $.entwine.RuleList()); 1734 | 1735 | rule = rulelist.addRule(selector, event); 1736 | rule.handler = name; 1737 | 1738 | this.bind_proxy(selector, name, capture); 1739 | } 1740 | }); 1741 | 1742 | var bindings = $.entwine.capture_bindings = {}; 1743 | 1744 | var event_proxy = function(event) { 1745 | return function(e) { 1746 | var namespace, capturelists, forevent, capturelist, rule, handler, sel; 1747 | 1748 | for (var k in $.entwine.namespaces) { 1749 | namespace = $.entwine.namespaces[k]; 1750 | capturelists = namespace.captures; 1751 | 1752 | if (capturelists && (forevent = capturelists[event])) { 1753 | for (var k in forevent) { 1754 | var capturelist = forevent[k]; 1755 | var triggered = namespace.$([]); 1756 | 1757 | // Stepping through each selector from most to least specific 1758 | var j = capturelist.length; 1759 | while (j--) { 1760 | rule = capturelist[j]; 1761 | handler = rule.handler; 1762 | sel = rule.selector.selector; 1763 | 1764 | var matching = namespace.$(sel).not(triggered); 1765 | matching[handler].apply(matching, arguments); 1766 | 1767 | triggered = triggered.add(matching); 1768 | } 1769 | } 1770 | } 1771 | } 1772 | } 1773 | }; 1774 | 1775 | var selector_proxy = function(selector, handler, includechildren) { 1776 | var matcher = $.selector(selector); 1777 | return function(e){ 1778 | if (matcher.matches(e.target)) return handler.apply(this, arguments); 1779 | } 1780 | }; 1781 | 1782 | var document_proxy = function(selector, handler, includechildren) { 1783 | return function(e){ 1784 | if (e.target === document) return handler.apply(this, arguments); 1785 | } 1786 | }; 1787 | 1788 | var window_proxy = function(selector, handler, includechildren) { 1789 | return function(e){ 1790 | if (e.target === window) return handler.apply(this, arguments); 1791 | } 1792 | }; 1793 | 1794 | var property_proxy = function(property, handler, includechildren) { 1795 | var matcher; 1796 | 1797 | return function(e){ 1798 | var match = this['get'+property](); 1799 | 1800 | if (typeof(match) == 'string') { 1801 | var matcher = (matcher && match == matcher.selector) ? matcher : $.selector(match); 1802 | if (matcher.matches(e.target)) return handler.apply(this, arguments); 1803 | } 1804 | else { 1805 | if ($.inArray(e.target, match) !== -1) return handler.apply(this, arguments); 1806 | } 1807 | } 1808 | }; 1809 | 1810 | $.entwine.Namespace.addHandler({ 1811 | order: 10, 1812 | 1813 | bind: function(selector, k, v) { 1814 | var match; 1815 | if ($.isPlainObject(v) && (match = k.match(/^from\s*(.*)/))) { 1816 | var from = match[1]; 1817 | var proxyGen; 1818 | 1819 | if (from.match(/[^\w]/)) proxyGen = selector_proxy; 1820 | else if (from == 'Window' || from == 'window') proxyGen = window_proxy; 1821 | else if (from == 'Document' || from == 'document') proxyGen = document_proxy; 1822 | else proxyGen = property_proxy; 1823 | 1824 | for (var onevent in v) { 1825 | var handler = v[onevent]; 1826 | match = onevent.match(/^on(.*)/); 1827 | var event = match[1]; 1828 | 1829 | this.bind_capture(selector, event, k + '_' + event, proxyGen(from, handler)); 1830 | 1831 | if (!bindings[event]) { 1832 | var namespaced = event.replace(/(\s+|$)/g, '.entwine$1'); 1833 | bindings[event] = event_proxy(event); 1834 | 1835 | $(proxyGen == window_proxy ? window : document).bind(namespaced, bindings[event]); 1836 | } 1837 | } 1838 | 1839 | return true; 1840 | } 1841 | } 1842 | }); 1843 | 1844 | })(jQuery); 1845 | ; 1846 | 1847 | 1848 | /* src/jquery.entwine.ctors.js */ 1849 | 1850 | (function($) { 1851 | 1852 | /* Add the methods to handle constructor & destructor binding to the Namespace class */ 1853 | $.entwine.Namespace.addMethods({ 1854 | bind_condesc: function(selector, name, func) { 1855 | var ctors = this.store.ctors || (this.store.ctors = $.entwine.RuleList()) ; 1856 | 1857 | var rule; 1858 | for (var i = 0 ; i < ctors.length; i++) { 1859 | if (ctors[i].selector.selector == selector.selector) { 1860 | rule = ctors[i]; break; 1861 | } 1862 | } 1863 | if (!rule) { 1864 | rule = ctors.addRule(selector, 'ctors'); 1865 | } 1866 | 1867 | rule[name] = func; 1868 | 1869 | if (!ctors[name+'proxy']) { 1870 | var one = this.one('ctors', name); 1871 | var namespace = this; 1872 | 1873 | var proxy = function(els, i, func) { 1874 | var j = els.length; 1875 | while (j--) { 1876 | var el = els[j]; 1877 | 1878 | var tmp_i = el.i, tmp_f = el.f; 1879 | el.i = i; el.f = one; 1880 | 1881 | try { func.call(namespace.$(el)); } 1882 | catch(e) { $.entwine.warn_exception(name, el, e); } 1883 | finally { el.i = tmp_i; el.f = tmp_f; } 1884 | } 1885 | }; 1886 | 1887 | ctors[name+'proxy'] = proxy; 1888 | } 1889 | } 1890 | }); 1891 | 1892 | $.entwine.Namespace.addHandler({ 1893 | order: 30, 1894 | 1895 | bind: function(selector, k, v) { 1896 | if ($.isFunction(v) && (k == 'onmatch' || k == 'onunmatch')) { 1897 | // When we add new matchers we need to trigger a full global recalc once, regardless of the DOM changes that triggered the event 1898 | this.matchersDirty = true; 1899 | 1900 | this.bind_condesc(selector, k, v); 1901 | return true; 1902 | } 1903 | } 1904 | }); 1905 | 1906 | /** 1907 | * Finds all the elements that now match a different rule (or have been removed) and call onmatch on onunmatch as appropriate 1908 | * 1909 | * Because this has to scan the DOM, and is therefore fairly slow, this is normally triggered off a short timeout, so that 1910 | * a series of DOM manipulations will only trigger this once. 1911 | * 1912 | * The downside of this is that things like: 1913 | * $('#foo').addClass('tabs'); $('#foo').tabFunctionBar(); 1914 | * won't work. 1915 | */ 1916 | $(document).bind('EntwineSubtreeMaybeChanged', function(e, changes){ 1917 | // var start = (new Date).getTime(); 1918 | 1919 | // For every namespace 1920 | for (var k in $.entwine.namespaces) { 1921 | var namespace = $.entwine.namespaces[k]; 1922 | 1923 | // That has constructors or destructors 1924 | var ctors = namespace.store.ctors; 1925 | if (ctors) { 1926 | 1927 | // Keep a record of elements that have matched some previous more specific rule. 1928 | // Not that we _don't_ actually do that until this is needed. If matched is null, it's not been calculated yet. 1929 | // We also keep track of any elements that have newly been taken or released by a specific rule 1930 | var matched = null, taken = $([]), released = $([]); 1931 | 1932 | // Updates matched to contain all the previously matched elements as if we'd been keeping track all along 1933 | var calcmatched = function(j){ 1934 | if (matched !== null) return; 1935 | matched = $([]); 1936 | 1937 | var cache, k = ctors.length; 1938 | while ((--k) > j) { 1939 | if (cache = ctors[k].cache) matched = matched.add(cache); 1940 | } 1941 | } 1942 | 1943 | // Some declared variables used in the loop 1944 | var add, rem, res, rule, sel, ctor, dtor, full; 1945 | 1946 | // Stepping through each selector from most to least specific 1947 | var j = ctors.length; 1948 | while (j--) { 1949 | // Build some quick-access variables 1950 | rule = ctors[j]; 1951 | sel = rule.selector.selector; 1952 | ctor = rule.onmatch; 1953 | dtor = rule.onunmatch; 1954 | 1955 | /* 1956 | Rule.cache might be stale or fresh. It'll be stale if 1957 | - some more specific selector now has some of rule.cache in it 1958 | - some change has happened that means new elements match this selector now 1959 | - some change has happened that means elements no longer match this selector 1960 | 1961 | The first we can just compare rules.cache with matched, removing anything that's there already. 1962 | */ 1963 | 1964 | // Reset the "elements that match this selector and no more specific selector with an onmatch rule" to null. 1965 | // Staying null means this selector is fresh. 1966 | res = null; 1967 | 1968 | // If this gets changed to true, it's too hard to do a delta update, so do a full update 1969 | full = false; 1970 | 1971 | if (namespace.matchersDirty || changes.global) { 1972 | // For now, just fall back to old version. We need to do something like changed.Subtree.find('*').andSelf().filter(sel), but that's _way_ slower on modern browsers than the below 1973 | full = true; 1974 | } 1975 | else { 1976 | // We don't deal with attributes yet, so any attribute change means we need to do a full recalc 1977 | for (var k in changes.attrs) { full = true; break; } 1978 | 1979 | /* 1980 | If a class changes, but it isn't listed in our selector, we don't care - the change couldn't affect whether or not any element matches 1981 | 1982 | If it is listed on our selector 1983 | - If it is on the direct match part, it could have added or removed the node it changed on 1984 | - If it is on the context part, it could have added or removed any node that were previously included or excluded because of a match or failure to match with the context required on that node 1985 | - NOTE: It might be on _both_ 1986 | */ 1987 | 1988 | var method = rule.selector.affectedBy(changes); 1989 | 1990 | if (method.classes.context) { 1991 | full = true; 1992 | } 1993 | else { 1994 | for (var k in method.classes.direct) { 1995 | calcmatched(j); 1996 | var recheck = changes.classes[k].not(matched); 1997 | 1998 | if (res === null) { 1999 | res = rule.cache ? rule.cache.not(taken).add(released.filter(sel)) : $([]); 2000 | } 2001 | 2002 | res = res.not(recheck).add(recheck.filter(sel)); 2003 | } 2004 | } 2005 | } 2006 | 2007 | if (full) { 2008 | calcmatched(j); 2009 | res = $(sel).not(matched); 2010 | } 2011 | else { 2012 | if (!res) { 2013 | // We weren't stale because of any changes to the DOM that affected this selector, but more specific 2014 | // onmatches might have caused stale-ness 2015 | 2016 | // Do any of the previous released elements match this selector? 2017 | add = released.length && released.filter(sel); 2018 | 2019 | if (add && add.length) { 2020 | // Yes, so we're stale as we need to include them. Filter for any possible taken value at the same time 2021 | res = rule.cache ? rule.cache.not(taken).add(add) : add; 2022 | } 2023 | else { 2024 | // Do we think we own any of the elements now taken by more specific rules? 2025 | rem = taken.length && rule.cache && rule.cache.filter(taken); 2026 | 2027 | if (rem && rem.length) { 2028 | // Yes, so we're stale as we need to exclude them. 2029 | res = rule.cache.not(rem); 2030 | } 2031 | } 2032 | } 2033 | } 2034 | 2035 | // Res will be null if we know we are fresh (no full needed, selector not affectedBy changes) 2036 | if (res === null) { 2037 | // If we are tracking matched, add ourselves 2038 | if (matched && rule.cache) matched = matched.add(rule.cache); 2039 | } 2040 | else { 2041 | // If this selector has a list of elements it matched against last time 2042 | if (rule.cache) { 2043 | // Find the ones that are extra this time 2044 | add = res.not(rule.cache); 2045 | rem = rule.cache.not(res); 2046 | } 2047 | else { 2048 | add = res; rem = null; 2049 | } 2050 | 2051 | if ((add && add.length) || (rem && rem.length)) { 2052 | if (rem && rem.length) { 2053 | released = released.add(rem); 2054 | 2055 | if (dtor && !rule.onunmatchRunning) { 2056 | rule.onunmatchRunning = true; 2057 | ctors.onunmatchproxy(rem, j, dtor); 2058 | rule.onunmatchRunning = false; 2059 | } 2060 | } 2061 | 2062 | // Call the constructor on the newly matched ones 2063 | if (add && add.length) { 2064 | taken = taken.add(add); 2065 | released = released.not(add); 2066 | 2067 | if (ctor && !rule.onmatchRunning) { 2068 | rule.onmatchRunning = true; 2069 | ctors.onmatchproxy(add, j, ctor); 2070 | rule.onmatchRunning = false; 2071 | } 2072 | } 2073 | } 2074 | 2075 | // If we are tracking matched, add ourselves 2076 | if (matched) matched = matched.add(res); 2077 | 2078 | // And remember this list of matching elements again this selector, so next matching we can find the unmatched ones 2079 | rule.cache = res; 2080 | } 2081 | } 2082 | 2083 | namespace.matchersDirty = false; 2084 | } 2085 | } 2086 | 2087 | // console.log((new Date).getTime() - start); 2088 | }); 2089 | 2090 | 2091 | })(jQuery); 2092 | ; 2093 | 2094 | 2095 | /* src/jquery.entwine.addrem.js */ 2096 | 2097 | (function($) { 2098 | 2099 | $.entwine.Namespace.addMethods({ 2100 | build_addrem_proxy: function(name) { 2101 | var one = this.one(name, 'func'); 2102 | 2103 | return function() { 2104 | if (this.length === 0){ 2105 | return; 2106 | } 2107 | else if (this.length) { 2108 | var rv, i = this.length; 2109 | while (i--) rv = one(this[i], arguments); 2110 | return rv; 2111 | } 2112 | else { 2113 | return one(this, arguments); 2114 | } 2115 | }; 2116 | }, 2117 | 2118 | bind_addrem_proxy: function(selector, name, func) { 2119 | var rulelist = this.store[name] || (this.store[name] = $.entwine.RuleList()); 2120 | 2121 | var rule = rulelist.addRule(selector, name); rule.func = func; 2122 | 2123 | if (!this.injectee.hasOwnProperty(name)) { 2124 | this.injectee[name] = this.build_addrem_proxy(name); 2125 | this.injectee[name].isentwinemethod = true; 2126 | } 2127 | } 2128 | }); 2129 | 2130 | $.entwine.Namespace.addHandler({ 2131 | order: 30, 2132 | 2133 | bind: function(selector, k, v) { 2134 | if ($.isFunction(v) && (k == 'onadd' || k == 'onremove')) { 2135 | this.bind_addrem_proxy(selector, k, v); 2136 | return true; 2137 | } 2138 | } 2139 | }); 2140 | 2141 | $(document).bind('EntwineElementsAdded', function(e){ 2142 | // For every namespace 2143 | for (var k in $.entwine.namespaces) { 2144 | var namespace = $.entwine.namespaces[k]; 2145 | if (namespace.injectee.onadd) namespace.injectee.onadd.call(e.targets); 2146 | } 2147 | }); 2148 | 2149 | $(document).bind('EntwineElementsRemoved', function(e){ 2150 | for (var k in $.entwine.namespaces) { 2151 | var namespace = $.entwine.namespaces[k]; 2152 | if (namespace.injectee.onremove) namespace.injectee.onremove.call(e.targets); 2153 | } 2154 | }); 2155 | 2156 | 2157 | 2158 | 2159 | })(jQuery); 2160 | ; 2161 | 2162 | 2163 | /* src/jquery.entwine.properties.js */ 2164 | 2165 | (function($) { 2166 | 2167 | var entwine_prepend = '__entwine!'; 2168 | 2169 | var getEntwineData = function(el, namespace, property) { 2170 | return el.data(entwine_prepend + namespace + '!' + property); 2171 | }; 2172 | 2173 | var setEntwineData = function(el, namespace, property, value) { 2174 | return el.data(entwine_prepend + namespace + '!' + property, value); 2175 | }; 2176 | 2177 | var getEntwineDataAsHash = function(el, namespace) { 2178 | var hash = {}; 2179 | var id = jQuery.data(el[0]); 2180 | 2181 | var matchstr = entwine_prepend + namespace + '!'; 2182 | var matchlen = matchstr.length; 2183 | 2184 | var cache = jQuery.cache[id]; 2185 | for (var k in cache) { 2186 | if (k.substr(0,matchlen) == matchstr) hash[k.substr(matchlen)] = cache[k]; 2187 | } 2188 | 2189 | return hash; 2190 | }; 2191 | 2192 | var setEntwineDataFromHash = function(el, namespace, hash) { 2193 | for (var k in hash) setEntwineData(namespace, k, hash[k]); 2194 | }; 2195 | 2196 | var entwineData = function(el, namespace, args) { 2197 | switch (args.length) { 2198 | case 0: 2199 | return getEntwineDataAsHash(el, namespace); 2200 | case 1: 2201 | if (typeof args[0] == 'string') return getEntwineData(el, namespace, args[0]); 2202 | else return setEntwineDataFromHash(el, namespace, args[0]); 2203 | default: 2204 | return setEntwineData(el, namespace, args[0], args[1]); 2205 | } 2206 | }; 2207 | 2208 | $.extend($.fn, { 2209 | entwineData: function() { 2210 | return entwineData(this, '__base', arguments); 2211 | } 2212 | }); 2213 | 2214 | $.entwine.Namespace.addHandler({ 2215 | order: 60, 2216 | 2217 | bind: function(selector, k, v) { 2218 | if (k.charAt(0) != k.charAt(0).toUpperCase()) $.entwine.warn('Entwine property '+k+' does not start with a capital letter', $.entwine.WARN_LEVEL_BESTPRACTISE); 2219 | 2220 | // Create the getters and setters 2221 | 2222 | var getterName = 'get'+k; 2223 | var setterName = 'set'+k; 2224 | 2225 | this.bind_proxy(selector, getterName, function() { var r = this.entwineData(k); return r === undefined ? v : r; }); 2226 | this.bind_proxy(selector, setterName, function(v){ return this.entwineData(k, v); }); 2227 | 2228 | // Get the get and set proxies we just created 2229 | 2230 | var getter = this.injectee[getterName]; 2231 | var setter = this.injectee[setterName]; 2232 | 2233 | // And bind in the jQuery-style accessor 2234 | 2235 | this.bind_proxy(selector, k, function(v){ return (arguments.length == 1 ? setter : getter).call(this, v) ; }); 2236 | 2237 | return true; 2238 | }, 2239 | 2240 | namespaceMethodOverrides: function(namespace){ 2241 | return { 2242 | entwineData: function() { 2243 | return entwineData(this, namespace.name, arguments); 2244 | } 2245 | }; 2246 | } 2247 | }); 2248 | 2249 | })(jQuery); 2250 | ; 2251 | 2252 | 2253 | /* src/jquery.entwine.legacy.js */ 2254 | 2255 | (function($) { 2256 | 2257 | // Adds back concrete methods for backwards compatibility 2258 | $.concrete = $.entwine; 2259 | $.fn.concrete = $.fn.entwine; 2260 | $.fn.concreteData = $.fn.entwineData; 2261 | 2262 | // Use addHandler to hack in the namespace.$.concrete equivilent to the namespace.$.entwine namespace-injection 2263 | $.entwine.Namespace.addHandler({ 2264 | order: 100, 2265 | bind: function(selector, k, v) { return false; }, 2266 | 2267 | namespaceMethodOverrides: function(namespace){ 2268 | namespace.$.concrete = namespace.$.entwine; 2269 | namespace.injectee.concrete = namespace.injectee.entwine; 2270 | namespace.injectee.concreteData = namespace.injectee.entwineData; 2271 | return {}; 2272 | } 2273 | }); 2274 | 2275 | })(jQuery); 2276 | ; 2277 | 2278 | -------------------------------------------------------------------------------- /javascript/externals/silverstripe/lib.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | // Copyright (c) 2011 John Resig, http://jquery.com/ 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | //define vars for interal use 25 | var $window = $( window ), 26 | $html = $( 'html' ), 27 | $head = $( 'head' ), 28 | 29 | //url path helpers for use in relative url management 30 | path = { 31 | 32 | // This scary looking regular expression parses an absolute URL or its relative 33 | // variants (protocol, site, document, query, and hash), into the various 34 | // components (protocol, host, path, query, fragment, etc that make up the 35 | // URL as well as some other commonly used sub-parts. When used with RegExp.exec() 36 | // or String.match, it parses the URL into a results array that looks like this: 37 | // 38 | // [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content 39 | // [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread 40 | // [2]: http://jblas:password@mycompany.com:8080/mail/inbox 41 | // [3]: http://jblas:password@mycompany.com:8080 42 | // [4]: http: 43 | // [5]: // 44 | // [6]: jblas:password@mycompany.com:8080 45 | // [7]: jblas:password 46 | // [8]: jblas 47 | // [9]: password 48 | // [10]: mycompany.com:8080 49 | // [11]: mycompany.com 50 | // [12]: 8080 51 | // [13]: /mail/inbox 52 | // [14]: /mail/ 53 | // [15]: inbox 54 | // [16]: ?msg=1234&type=unread 55 | // [17]: #msg-content 56 | // 57 | urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/, 58 | 59 | //Parse a URL into a structure that allows easy access to 60 | //all of the URL components by name. 61 | parseUrl: function( url ) { 62 | // If we're passed an object, we'll assume that it is 63 | // a parsed url object and just return it back to the caller. 64 | if ( $.type( url ) === "object" ) { 65 | return url; 66 | } 67 | 68 | var matches = path.urlParseRE.exec( url || "" ) || []; 69 | 70 | // Create an object that allows the caller to access the sub-matches 71 | // by name. Note that IE returns an empty string instead of undefined, 72 | // like all other browsers do, so we normalize everything so its consistent 73 | // no matter what browser we're running on. 74 | return { 75 | href: matches[ 0 ] || "", 76 | hrefNoHash: matches[ 1 ] || "", 77 | hrefNoSearch: matches[ 2 ] || "", 78 | domain: matches[ 3 ] || "", 79 | protocol: matches[ 4 ] || "", 80 | doubleSlash: matches[ 5 ] || "", 81 | authority: matches[ 6 ] || "", 82 | username: matches[ 8 ] || "", 83 | password: matches[ 9 ] || "", 84 | host: matches[ 10 ] || "", 85 | hostname: matches[ 11 ] || "", 86 | port: matches[ 12 ] || "", 87 | pathname: matches[ 13 ] || "", 88 | directory: matches[ 14 ] || "", 89 | filename: matches[ 15 ] || "", 90 | search: matches[ 16 ] || "", 91 | hash: matches[ 17 ] || "" 92 | }; 93 | }, 94 | 95 | //Turn relPath into an asbolute path. absPath is 96 | //an optional absolute path which describes what 97 | //relPath is relative to. 98 | makePathAbsolute: function( relPath, absPath ) { 99 | if ( relPath && relPath.charAt( 0 ) === "/" ) { 100 | return relPath; 101 | } 102 | 103 | relPath = relPath || ""; 104 | absPath = absPath ? absPath.replace( /^\/|(\/[^\/]*|[^\/]+)$/g, "" ) : ""; 105 | 106 | var absStack = absPath ? absPath.split( "/" ) : [], 107 | relStack = relPath.split( "/" ); 108 | for ( var i = 0; i < relStack.length; i++ ) { 109 | var d = relStack[ i ]; 110 | switch ( d ) { 111 | case ".": 112 | break; 113 | case "..": 114 | if ( absStack.length ) { 115 | absStack.pop(); 116 | } 117 | break; 118 | default: 119 | absStack.push( d ); 120 | break; 121 | } 122 | } 123 | return "/" + absStack.join( "/" ); 124 | }, 125 | 126 | //Returns true if both urls have the same domain. 127 | isSameDomain: function( absUrl1, absUrl2 ) { 128 | return path.parseUrl( absUrl1 ).domain === path.parseUrl( absUrl2 ).domain; 129 | }, 130 | 131 | //Returns true for any relative variant. 132 | isRelativeUrl: function( url ) { 133 | // All relative Url variants have one thing in common, no protocol. 134 | return path.parseUrl( url ).protocol === ""; 135 | }, 136 | 137 | //Returns true for an absolute url. 138 | isAbsoluteUrl: function( url ) { 139 | return path.parseUrl( url ).protocol !== ""; 140 | }, 141 | 142 | //Turn the specified realtive URL into an absolute one. This function 143 | //can handle all relative variants (protocol, site, document, query, fragment). 144 | makeUrlAbsolute: function( relUrl, absUrl ) { 145 | if ( !path.isRelativeUrl( relUrl ) ) { 146 | return relUrl; 147 | } 148 | 149 | var relObj = path.parseUrl( relUrl ), 150 | absObj = path.parseUrl( absUrl ), 151 | protocol = relObj.protocol || absObj.protocol, 152 | doubleSlash = relObj.protocol ? relObj.doubleSlash : ( relObj.doubleSlash || absObj.doubleSlash ), 153 | authority = relObj.authority || absObj.authority, 154 | hasPath = relObj.pathname !== "", 155 | pathname = path.makePathAbsolute( relObj.pathname || absObj.filename, absObj.pathname ), 156 | search = relObj.search || ( !hasPath && absObj.search ) || "", 157 | hash = relObj.hash; 158 | 159 | return protocol + doubleSlash + authority + pathname + search + hash; 160 | }, 161 | 162 | //Add search (aka query) params to the specified url. 163 | // 2013-12-06 ischommer: Customized to merge with existing keys 164 | addSearchParams: function( url, params ) { 165 | var u = path.parseUrl( url ), 166 | params = ( typeof params === "string" ) ? path.convertSearchToArray( params ) : params, 167 | newParams = $.extend( path.convertSearchToArray( u.search ), params ); 168 | return u.hrefNoSearch + '?' + $.param( newParams ) + ( u.hash || "" ); 169 | }, 170 | 171 | // 2013-12-06 ischommer: Added to allow merge with existing keys 172 | getSearchParams: function(url) { 173 | var u = path.parseUrl( url ); 174 | return path.convertSearchToArray( u.search ); 175 | }, 176 | 177 | // Converts query strings (foo=bar&baz=bla) to a hash. 178 | // TODO Handle repeating elements (e.g. arr[]=one&arr[]=two) 179 | // 2013-12-06 ischommer: Added to allow merge with existing keys 180 | convertSearchToArray: function(search) { 181 | var params = {}, 182 | search = search.replace( /^\?/, '' ), 183 | parts = search ? search.split( '&' ) : [], i, tmp; 184 | for(i=0; i < parts.length; i++) { 185 | tmp = parts[i].split( '=' ); 186 | params[tmp[0]] = tmp[1]; 187 | } 188 | return params; 189 | }, 190 | 191 | convertUrlToDataUrl: function( absUrl ) { 192 | var u = path.parseUrl( absUrl ); 193 | if ( path.isEmbeddedPage( u ) ) { 194 | // For embedded pages, remove the dialog hash key as in getFilePath(), 195 | // otherwise the Data Url won't match the id of the embedded Page. 196 | return u.hash.split( dialogHashKey )[0].replace( /^#/, "" ); 197 | } else if ( path.isSameDomain( u, document ) ) { 198 | return u.hrefNoHash.replace( document.domain, "" ); 199 | } 200 | return absUrl; 201 | }, 202 | 203 | //get path from current hash, or from a file path 204 | get: function( newPath ) { 205 | if( newPath === undefined ) { 206 | newPath = location.hash; 207 | } 208 | return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' ); 209 | }, 210 | 211 | //return the substring of a filepath before the sub-page key, for making a server request 212 | getFilePath: function( path ) { 213 | var splitkey = '&' + $.mobile.subPageUrlKey; 214 | return path && path.split( splitkey )[0].split( dialogHashKey )[0]; 215 | }, 216 | 217 | //set location hash to path 218 | set: function( path ) { 219 | location.hash = path; 220 | }, 221 | 222 | //test if a given url (string) is a path 223 | //NOTE might be exceptionally naive 224 | isPath: function( url ) { 225 | return ( /\// ).test( url ); 226 | }, 227 | 228 | //return a url path with the window's location protocol/hostname/pathname removed 229 | clean: function( url ) { 230 | return url.replace( document.domain, "" ); 231 | }, 232 | 233 | //just return the url without an initial # 234 | stripHash: function( url ) { 235 | return url.replace( /^#/, "" ); 236 | }, 237 | 238 | //remove the preceding hash, any query params, and dialog notations 239 | cleanHash: function( hash ) { 240 | return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) ); 241 | }, 242 | 243 | //check whether a url is referencing the same domain, or an external domain or different protocol 244 | //could be mailto, etc 245 | isExternal: function( url ) { 246 | var u = path.parseUrl( url ); 247 | return u.protocol && u.domain !== document.domain ? true : false; 248 | }, 249 | 250 | hasProtocol: function( url ) { 251 | return ( /^(:?\w+:)/ ).test( url ); 252 | } 253 | }; 254 | 255 | $.path = path; 256 | }(jQuery)); 257 | -------------------------------------------------------------------------------- /javascript/externals/silverstripe/ssui.core.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | /** 4 | * Allows icon definition via HTML5 data attrs for easier handling in PHP. 5 | * 6 | * Adds an alternative appearance so we can toggle back and forth between them 7 | * and register event handlers to add custom styling and behaviour. Example use 8 | * is in the CMS with the saving buttons - depending on the page's state one of 9 | * them will either say "Save draft" or "Saved", and will have different colour. 10 | */ 11 | $.widget('ssui.button', $.ui.button, { 12 | options: { 13 | alternate: { 14 | icon: null, 15 | text: null 16 | }, 17 | showingAlternate: false 18 | }, 19 | 20 | /** 21 | * Switch between the alternate appearances. 22 | */ 23 | toggleAlternate: function() { 24 | if (this._trigger('ontogglealternate')===false) return; 25 | 26 | // Only switch to alternate if it has been enabled through options. 27 | if (!this.options.alternate.icon && !this.options.alternate.text) return; 28 | 29 | this.options.showingAlternate = !this.options.showingAlternate; 30 | this.refresh(); 31 | }, 32 | 33 | /** 34 | * Adjust the appearance to fit with the current settings. 35 | */ 36 | _refreshAlternate: function() { 37 | this._trigger('beforerefreshalternate'); 38 | 39 | // Only switch to alternate if it has been enabled through options. 40 | if (!this.options.alternate.icon && !this.options.alternate.text) return; 41 | 42 | if (this.options.showingAlternate) { 43 | this.element.find('.ui-button-icon-primary').hide(); 44 | this.element.find('.ui-button-text').hide(); 45 | this.element.find('.ui-button-icon-alternate').show(); 46 | this.element.find('.ui-button-text-alternate').show(); 47 | } 48 | else { 49 | this.element.find('.ui-button-icon-primary').show(); 50 | this.element.find('.ui-button-text').show(); 51 | this.element.find('.ui-button-icon-alternate').hide(); 52 | this.element.find('.ui-button-text-alternate').hide(); 53 | } 54 | 55 | this._trigger('afterrefreshalternate'); 56 | }, 57 | 58 | /** 59 | * Construct button - pulls in options from data attributes. 60 | * Injects new elements for alternate appearance (if requested via options). 61 | */ 62 | _resetButton: function() { 63 | var iconPrimary = this.element.data('icon-primary'), 64 | iconSecondary = this.element.data('icon-secondary'); 65 | 66 | if (!iconPrimary) iconPrimary = this.element.data('icon'); 67 | 68 | // TODO Move prefix out of this method, without requriing it for every icon definition in a data attr 69 | if(iconPrimary) this.options.icons.primary = 'btn-icon-' + iconPrimary; 70 | if(iconSecondary) this.options.icons.secondary = 'btn-icon-' + iconSecondary; 71 | 72 | $.ui.button.prototype._resetButton.call(this); 73 | 74 | // Pull options from data attributes. Overriden by explicit options given on widget creation. 75 | if (!this.options.alternate.text) { 76 | this.options.alternate.text = this.element.data('text-alternate'); 77 | } 78 | if (!this.options.alternate.icon) { 79 | this.options.alternate.icon = this.element.data('icon-alternate'); 80 | } 81 | if (!this.options.showingAlternate) { 82 | this.options.showingAlternate = this.element.hasClass('ss-ui-alternate'); 83 | } 84 | 85 | // Create missing elements. 86 | if (this.options.alternate.icon) { 87 | this.buttonElement.append( 88 | "" 90 | ); 91 | } 92 | if (this.options.alternate.text) { 93 | this.buttonElement.append( 94 | "" + this.options.alternate.text + "" 95 | ); 96 | } 97 | 98 | this._refreshAlternate(); 99 | }, 100 | 101 | refresh: function() { 102 | $.ui.button.prototype.refresh.call(this); 103 | 104 | this._refreshAlternate(); 105 | }, 106 | 107 | destroy: function() { 108 | this.element.find('.ui-button-text-alternate').remove(); 109 | this.element.find('.ui-button-icon-alternate').remove(); 110 | 111 | $.ui.button.prototype.destroy.call( this ); 112 | } 113 | }); 114 | 115 | /** 116 | * Extends jQueryUI dialog with iframe abilities (and related resizing logic), 117 | * and sets some CMS-wide defaults. 118 | * 119 | * Additional settings: 120 | * - 'autoPosition': Automatically reposition window on resize based on 'position' option 121 | * - 'widthRatio': Sets width based on percentage of window (value between 0 and 1) 122 | * - 'heightRatio': Sets width based on percentage of window (value between 0 and 1) 123 | * - 'reloadOnOpen': Reloads the iframe whenever the dialog is reopened 124 | * - 'iframeUrl': Create an iframe element and load this URL when the dialog is created 125 | */ 126 | $.widget("ssui.ssdialog", $.ui.dialog, { 127 | options: { 128 | // Custom properties 129 | iframeUrl: '', 130 | reloadOnOpen: true, 131 | dialogExtraClass: '', 132 | 133 | // Defaults 134 | modal: true, 135 | bgiframe: true, 136 | autoOpen: false, 137 | autoPosition: true, 138 | minWidth: 500, 139 | maxWidth: 700, 140 | minHeight: 300, 141 | maxHeight: 600, 142 | widthRatio: 0.8, 143 | heightRatio: 0.8, 144 | resizable: false 145 | }, 146 | _create: function() { 147 | $.ui.dialog.prototype._create.call(this); 148 | 149 | var self = this; 150 | 151 | // Create iframe 152 | var iframe = $(''); 153 | iframe.bind('load', function(e) { 154 | if($(this).attr('src') == 'about:blank') return; 155 | 156 | iframe.addClass('loaded').show(); // more reliable than 'src' attr check (in IE) 157 | self._resizeIframe(); 158 | self.uiDialog.removeClass('loading'); 159 | }).hide(); 160 | 161 | if(this.options.dialogExtraClass) this.uiDialog.addClass(this.options.dialogExtraClass); 162 | this.element.append(iframe); 163 | 164 | // Let the iframe handle its scrolling 165 | if(this.options.iframeUrl) this.element.css('overflow', 'hidden'); 166 | }, 167 | open: function() { 168 | $.ui.dialog.prototype.open.call(this); 169 | 170 | var self = this, iframe = this.element.children('iframe'); 171 | 172 | // Load iframe 173 | if(this.options.iframeUrl && (!iframe.hasClass('loaded') || this.options.reloadOnOpen)) { 174 | iframe.hide(); 175 | iframe.attr('src', this.options.iframeUrl); 176 | this.uiDialog.addClass('loading'); 177 | } 178 | 179 | // Resize events 180 | $(window).bind('resize.ssdialog', function() {self._resizeIframe();}); 181 | }, 182 | close: function() { 183 | $.ui.dialog.prototype.close.call(this); 184 | 185 | this.uiDialog.unbind('resize.ssdialog'); 186 | $(window).unbind('resize.ssdialog'); 187 | }, 188 | _resizeIframe: function() { 189 | var opts = {}, newWidth, newHeight, iframe = this.element.children('iframe');; 190 | if(this.options.widthRatio) { 191 | newWidth = $(window).width() * this.options.widthRatio; 192 | if(this.options.minWidth && newWidth < this.options.minWidth) { 193 | opts.width = this.options.minWidth 194 | } else if(this.options.maxWidth && newWidth > this.options.maxWidth) { 195 | opts.width = this.options.maxWidth; 196 | } else { 197 | opts.width = newWidth; 198 | } 199 | } 200 | if(this.options.heightRatio) { 201 | newHeight = $(window).height() * this.options.heightRatio; 202 | if(this.options.minHeight && newHeight < this.options.minHeight) { 203 | opts.height = this.options.minHeight 204 | } else if(this.options.maxHeight && newHeight > this.options.maxHeight) { 205 | opts.height = this.options.maxHeight; 206 | } else { 207 | opts.height = newHeight; 208 | } 209 | } 210 | 211 | if(!jQuery.isEmptyObject(opts)) { 212 | this._setOptions(opts); 213 | 214 | // Resize iframe within dialog 215 | iframe.attr('width', 216 | opts.width 217 | - parseFloat(this.element.css('paddingLeft')) 218 | - parseFloat(this.element.css('paddingRight')) 219 | ); 220 | iframe.attr('height', 221 | opts.height 222 | - parseFloat(this.element.css('paddingTop')) 223 | - parseFloat(this.element.css('paddingBottom')) 224 | ); 225 | 226 | // Enforce new position 227 | if(this.options.autoPosition) { 228 | this._setOption("position", this.options.position); 229 | } 230 | } 231 | } 232 | }); 233 | 234 | $.widget("ssui.titlebar", { 235 | _create: function() { 236 | this.originalTitle = this.element.attr('title'); 237 | 238 | var self = this; 239 | var options = this.options; 240 | 241 | var title = options.title || this.originalTitle || ' '; 242 | var titleId = $.ui.dialog.getTitleId(this.element); 243 | 244 | this.element.parent().addClass('ui-dialog'); 245 | 246 | var uiDialogTitlebar = this.element. 247 | addClass( 248 | 'ui-dialog-titlebar ' + 249 | 'ui-widget-header ' + 250 | 'ui-corner-all ' + 251 | 'ui-helper-clearfix' 252 | ); 253 | 254 | // By default, the 255 | 256 | if(options.closeButton) { 257 | var uiDialogTitlebarClose = $('') 258 | .addClass( 259 | 'ui-dialog-titlebar-close ' + 260 | 'ui-corner-all' 261 | ) 262 | .attr('role', 'button') 263 | .hover( 264 | function() { 265 | uiDialogTitlebarClose.addClass('ui-state-hover'); 266 | }, 267 | function() { 268 | uiDialogTitlebarClose.removeClass('ui-state-hover'); 269 | } 270 | ) 271 | .focus(function() { 272 | uiDialogTitlebarClose.addClass('ui-state-focus'); 273 | }) 274 | .blur(function() { 275 | uiDialogTitlebarClose.removeClass('ui-state-focus'); 276 | }) 277 | .mousedown(function(ev) { 278 | ev.stopPropagation(); 279 | }) 280 | .appendTo(uiDialogTitlebar); 281 | 282 | var uiDialogTitlebarCloseText = (this.uiDialogTitlebarCloseText = $('')) 283 | .addClass( 284 | 'ui-icon ' + 285 | 'ui-icon-closethick' 286 | ) 287 | .text(options.closeText) 288 | .appendTo(uiDialogTitlebarClose); 289 | } 290 | 291 | var uiDialogTitle = $('') 292 | .addClass('ui-dialog-title') 293 | .attr('id', titleId) 294 | .html(title) 295 | .prependTo(uiDialogTitlebar); 296 | 297 | uiDialogTitlebar.find("*").add(uiDialogTitlebar).disableSelection(); 298 | }, 299 | 300 | destroy: function() { 301 | this.element 302 | .unbind('.dialog') 303 | .removeData('dialog') 304 | .removeClass('ui-dialog-content ui-widget-content') 305 | .hide().appendTo('body'); 306 | 307 | (this.originalTitle && this.element.attr('title', this.originalTitle)); 308 | } 309 | }); 310 | 311 | $.extend($.ssui.titlebar, { 312 | version: "0.0.1", 313 | options: { 314 | title: '', 315 | closeButton: false, 316 | closeText: 'close' 317 | }, 318 | 319 | uuid: 0, 320 | 321 | getTitleId: function($el) { 322 | return 'ui-dialog-title-' + ($el.attr('id') || ++this.uuid); 323 | } 324 | }); 325 | }(jQuery)); 326 | -------------------------------------------------------------------------------- /src/Forms/GridField/GridField.php: -------------------------------------------------------------------------------- 1 | getUseAdminAPI()) { 33 | Requirements::javascriptTemplate(dirname(__FILE__) . '/../../../javascript/boot.template.js', [ 34 | 'SecurityID' => Convert::raw2js(SecurityToken::inst()->getValue()), 35 | 'AbsoluteBaseURL' => Convert::raw2js(Director::absoluteBaseURL()), 36 | 'BaseURL' => Convert::raw2js(Director::baseURL()), 37 | 'Environment' => Convert::raw2js(Director::get_environment_type()), 38 | 'Debugging' => (Director::isDev() ? 'true' : 'false') 39 | ]); 40 | 41 | Requirements::javascript('silverstripe/admin: client/dist/js/vendor.js'); 42 | Requirements::javascript('silverstripe/admin: client/dist/js/bundle.js'); 43 | Requirements::add_i18n_javascript('silverstripe/admin: javascript/lang'); 44 | } else { 45 | Requirements::javascript('https://code.jquery.com/jquery-3.7.0.min.js'); 46 | Requirements::javascript('https://code.jquery.com/jquery-migrate-1.4.1.min.js'); 47 | Requirements::javascript('silverstripe/admin: thirdparty/jquery-ui/jquery-ui.js'); 48 | Requirements::javascript('webbuilders-group/silverstripe-frontendgridfield: javascript/externals/hafriedlander/jquery-entwine/jquery.entwine-dist.js'); 49 | Requirements::javascript('silverstripe/admin: client/dist/js/i18n.js'); 50 | Requirements::add_i18n_javascript('silverstripe/admin: javascript/lang'); 51 | Requirements::javascript('webbuilders-group/silverstripe-frontendgridfield: javascript/externals/silverstripe/lib.js'); 52 | Requirements::javascript('webbuilders-group/silverstripe-frontendgridfield: javascript/externals/silverstripe/ssui.core.js'); 53 | Requirements::javascript('webbuilders-group/silverstripe-frontendgridfield: javascript/GridField.js'); 54 | } 55 | 56 | Requirements::javascript('webbuilders-group/silverstripe-frontendgridfield: javascript/FrontEndGridField.js'); 57 | 58 | 59 | return parent::FieldHolder(); 60 | } 61 | 62 | /** 63 | * Allow this GridField to use the Admin's API, you should not mix and match this otherwise you will run into issues! 64 | * @param bool $value Whether or not to use the admin's api 65 | * @return GridField 66 | */ 67 | public function setUseAdminAPI($value) 68 | { 69 | $this->useAdminAPI = $value; 70 | return $this; 71 | } 72 | 73 | /** 74 | * Get's whether this GridField is to use the Admin's API 75 | * @return Whether or not to use the admin's api 76 | */ 77 | public function getUseAdminAPI() 78 | { 79 | return $this->useAdminAPI || $this->config()->use_admin_api; 80 | } 81 | 82 | /** 83 | * Gets the type for this field 84 | * @return string 85 | */ 86 | public function Type() 87 | { 88 | return 'frontendgrid ' . parent::Type(); 89 | } 90 | 91 | /** 92 | * Custom Readonly transformation to remove actions which shouldn't be present for a readonly state. 93 | * @return GridField 94 | */ 95 | public function performReadonlyTransformation() 96 | { 97 | $this->readonlyComponents[] = GridFieldDetailForm::class; 98 | 99 | return parent::performReadonlyTransformation(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Forms/GridField/GridFieldConfig_Base.php: -------------------------------------------------------------------------------- 1 | use_admin_api) { 18 | $this->removeComponentsByType(GridField_ActionMenu::class); 19 | } 20 | 21 | //Use the legacy filter header as the GraphQL/React one will not work 22 | $filterHeader = $this->getComponentByType(GridFieldFilterHeader::class); 23 | if ($filterHeader) { 24 | if (property_exists($filterHeader, 'useLegacyFilterHeader')) { 25 | $filterHeader->useLegacyFilterHeader = true; 26 | } else { 27 | $this->removeComponent($filterHeader); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Forms/GridField/GridFieldConfig_RecordEditor.php: -------------------------------------------------------------------------------- 1 | removeComponentsByType(SS_GridFieldDetailForm::class) 20 | ->addComponent(new GridFieldDetailForm()); 21 | 22 | if (!GridField::config()->use_admin_api) { 23 | $this->removeComponentsByType(GridField_ActionMenu::class); 24 | } 25 | 26 | //Use the legacy filter header as the GraphQL/React one will not work 27 | $filterHeader = $this->getComponentByType(GridFieldFilterHeader::class); 28 | if ($filterHeader) { 29 | if (property_exists($filterHeader, 'useLegacyFilterHeader')) { 30 | $filterHeader->useLegacyFilterHeader = true; 31 | } else { 32 | $this->removeComponent($filterHeader); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Forms/GridField/GridFieldConfig_RecordViewer.php: -------------------------------------------------------------------------------- 1 | removeComponentsByType(SS_GridFieldDetailForm::class) 20 | ->addComponent(new GridFieldDetailForm()); 21 | 22 | if (!GridField::config()->use_admin_api) { 23 | $this->removeComponentsByType(GridField_ActionMenu::class); 24 | } 25 | 26 | //Use the legacy filter header as the GraphQL/React one will not work 27 | $filterHeader = $this->getComponentByType(GridFieldFilterHeader::class); 28 | if ($filterHeader) { 29 | if (property_exists($filterHeader, 'useLegacyFilterHeader')) { 30 | $filterHeader->useLegacyFilterHeader = true; 31 | } else { 32 | $this->removeComponent($filterHeader); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Forms/GridField/GridFieldConfig_RelationEditor.php: -------------------------------------------------------------------------------- 1 | removeComponentsByType(SS_GridFieldDetailForm::class) 20 | ->addComponent(new GridFieldDetailForm()); 21 | 22 | if (!GridField::config()->use_admin_api) { 23 | $this->removeComponentsByType(GridField_ActionMenu::class); 24 | } 25 | 26 | //Use the legacy filter header as the GraphQL/React one will not work 27 | $filterHeader = $this->getComponentByType(GridFieldFilterHeader::class); 28 | if ($filterHeader) { 29 | if (property_exists($filterHeader, 'useLegacyFilterHeader')) { 30 | $filterHeader->useLegacyFilterHeader = true; 31 | } else { 32 | $this->removeComponent($filterHeader); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Forms/GridField/GridFieldDetailForm.php: -------------------------------------------------------------------------------- 1 | gridField->getList(); 34 | 35 | if (empty($this->record)) { 36 | $controller = $this->getToplevelController(); 37 | $noActionURL = $controller->removeAction($controller->getRequest()->getURL(true)); 38 | 39 | $controller->getResponse()->removeHeader('Location'); //clear the existing redirect 40 | 41 | return $controller->redirect($noActionURL, 302); 42 | } 43 | 44 | $canView = $this->record->canView(); 45 | $canEdit = $this->record->canEdit(); 46 | $canDelete = $this->record->canDelete(); 47 | $canCreate = $this->record->canCreate(); 48 | 49 | if (!$canView) { 50 | return $this->getToplevelController()->httpError(403); 51 | } 52 | 53 | $actions = new FieldList(); 54 | if ($this->record->ID !== 0) { 55 | if ($canEdit) { 56 | $actions->push( 57 | FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save')) 58 | ->setUseButtonTag(true) 59 | ->addExtraClass('btn-primary font-icon-save') 60 | ->setAttribute('data-icon', 'accept') 61 | ); 62 | } 63 | 64 | if ($canDelete) { 65 | $actions->push( 66 | FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete')) 67 | ->setUseButtonTag(true) 68 | ->addExtraClass('btn-outline-danger btn-hide-outline font-icon-trash-bin action-delete') 69 | ); 70 | } 71 | } else { // adding new record 72 | //Change the Save label to 'Create' 73 | $actions->push( 74 | FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create')) 75 | ->setUseButtonTag(true) 76 | ->addExtraClass('btn-primary font-icon-plus-thin') 77 | ->setAttribute('data-icon', 'add') 78 | ); 79 | 80 | // Add a Cancel link which is a button-like link and link back to one level up. 81 | $curmbs = $this->Breadcrumbs(); 82 | if ($curmbs && $curmbs->count() >= 2) { 83 | $one_level_up = $curmbs->offsetGet($curmbs->count() - 2); 84 | $text = sprintf( 85 | "%s", 86 | "crumb ss-ui-button btn-outline-danger btn-hide-outline font-icon-trash-bin cms-panel-link ui-corner-all", // CSS classes 87 | $one_level_up->Link, // url 88 | _t('GridFieldDetailForm.CancelBtn', 'Cancel') // label 89 | ); 90 | 91 | $actions->push(new LiteralField('cancelbutton', $text)); 92 | } 93 | } 94 | 95 | // If we are creating a new record in a has-many list, then 96 | // pre-populate the record's foreign key. 97 | if ($list instanceof HasManyList && !$this->record->isInDB()) { 98 | $key = $list->getForeignKey(); 99 | $id = $list->getForeignID(); 100 | $this->record->$key = $id; 101 | } 102 | 103 | $fields = $this->component->getFields(); 104 | if (!$fields) { 105 | $fields = ($this->record->hasMethod('getFrontEndFields') ? $this->record->getFrontEndFields() : $this->record->getCMSFields()); 106 | } 107 | 108 | // If we are creating a new record in a has-many list, then 109 | // Disable the form field as it has no effect. 110 | if ($list instanceof HasManyList) { 111 | $key = $list->getForeignKey(); 112 | 113 | if ($field = $fields->dataFieldByName($key)) { 114 | $fields->makeFieldReadonly($field); 115 | } 116 | } 117 | 118 | // this pushes the current page ID in as a hidden field 119 | // this means the request will have the current page ID in it 120 | // rather than relying on session which can have been rewritten 121 | // by the user having another tab open 122 | // see LeftAndMain::currentPageID 123 | if ($this->controller->hasMethod('currentPageID') && $this->controller->currentPageID()) { 124 | $fields->push(new HiddenField('CMSMainCurrentPageID', null, $this->controller->currentPageID())); 125 | } 126 | 127 | // Caution: API violation. Form expects a Controller, but we are giving it a RequestHandler instead. 128 | // Thanks to this however, we are able to nest GridFields, and also access the initial Controller by 129 | // dereferencing GridFieldDetailForm_ItemRequest->getController() multiple times. See getToplevelController 130 | // below. 131 | $form = new Form( 132 | $this, 133 | 'ItemEditForm', 134 | $fields, 135 | $actions, 136 | $this->component->getValidator() 137 | ); 138 | 139 | $form->loadDataFrom($this->record, ($this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT)); 140 | 141 | if ($this->record->ID && !$canEdit) { 142 | // Restrict editing of existing records 143 | $form->makeReadonly(); 144 | 145 | // Hack to re-enable delete button if user can delete 146 | if ($canDelete) { 147 | $form->Actions()->fieldByName('action_doDelete')->setReadonly(false); 148 | } 149 | } else if (!$this->record->ID && !$canCreate) { 150 | // Restrict creation of new records 151 | $form->makeReadonly(); 152 | } 153 | 154 | // Load many_many extraData for record. 155 | // Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields(). 156 | if ($list instanceof ManyManyList) { 157 | $extraData = $list->getExtraData('', $this->record->ID); 158 | $form->loadDataFrom(['ManyMany' => $extraData]); 159 | } 160 | 161 | $cb = $this->component->getItemEditFormCallback(); 162 | if ($cb) { 163 | $cb($form, $this); 164 | } 165 | 166 | $this->extend("updateItemEditForm", $form); 167 | return $form; 168 | } 169 | 170 | /** 171 | * Renders the view form 172 | * @param {SS_HTTPRequest} $request Request data 173 | * @return {string} Rendered view form 174 | */ 175 | public function view($request) 176 | { 177 | if (!$this->record->canView()) { 178 | $this->httpError(403); 179 | } 180 | 181 | $controller = $this->getToplevelController(); 182 | $form = $this->ItemEditForm($this->gridField, $request); 183 | 184 | if (!is_a($form, Form::class)) { 185 | return $form; 186 | } 187 | 188 | $form->makeReadonly(); 189 | 190 | 191 | return $controller->customise([ 192 | 'Title' => ($this->record && $this->record->exists() ? $this->record->Title : sprintf(_t('GridField.NewRecord', 'New %s'), singleton($this->gridField->getModelClass())->i18n_singular_name())), 193 | 'ItemEditForm' => $form, 194 | ])->renderWith($this->template); 195 | } 196 | 197 | /** 198 | * Renders the edit form 199 | * @param {SS_HTTPRequest} $request Request data 200 | * @return {string} Rendered edit form 201 | */ 202 | public function edit($request) 203 | { 204 | $controller = $this->getToplevelController(); 205 | $form = $this->ItemEditForm($this->gridField, $request); 206 | 207 | if (!is_a($form, Form::class)) { 208 | return $form; 209 | } 210 | 211 | return $controller->customise([ 212 | 'Title' => ($this->record && $this->record->exists() ? $this->record->Title : sprintf(_t('GridField.NewRecord', 'New %s'), singleton($this->gridField->getModelClass())->i18n_singular_name())), 213 | 'ItemEditForm' => $form, 214 | ])->renderWith($this->template); 215 | } 216 | 217 | /** 218 | * Disabled, the front end does not use breadcrumbs to remember the paths 219 | */ 220 | public function Breadcrumbs($unlinked = false) 221 | { 222 | return; 223 | } 224 | 225 | public function doSave($data, $form) 226 | { 227 | $new_record = $this->record->ID == 0; 228 | $controller = $this->getToplevelController(); 229 | $list = $this->gridField->getList(); 230 | 231 | if (!$this->record->canEdit()) { 232 | return $controller->httpError(403); 233 | } 234 | 235 | if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) { 236 | $newClassName = $data['ClassName']; 237 | // The records originally saved attribute was overwritten by $form->saveInto($record) before. 238 | // This is necessary for newClassInstance() to work as expected, and trigger change detection 239 | // on the ClassName attribute 240 | $this->record->setClassName($this->record->ClassName); 241 | // Replace $record with a new instance 242 | $this->record = $this->record->newClassInstance($newClassName); 243 | } 244 | 245 | try { 246 | $form->saveInto($this->record); 247 | $this->record->write(); 248 | $extraData = $this->getExtraSavedData($this->record, $list); 249 | $list->add($this->record, $extraData); 250 | } catch (ValidationException $e) { 251 | $form->setSessionValidationResult($e->getResult()); 252 | 253 | $controller->getRequest()->getSession()->set("FormInfo.{$form->FormName()}.data", $form->getData()); 254 | 255 | return $controller->redirectBack(); 256 | } 257 | 258 | // TODO Save this item into the given relationship 259 | 260 | $link = '"' . htmlspecialchars($this->record->Title, ENT_QUOTES) . '"'; 261 | $message = _t( 262 | 'GridFieldDetailForm.Saved', 263 | 'Saved {name} {link}', 264 | [ 265 | 'name' => $this->record->i18n_singular_name(), 266 | 'link' => $link, 267 | ] 268 | ); 269 | 270 | $form->sessionMessage($message, ValidationResult::TYPE_GOOD, ValidationResult::CAST_HTML); 271 | 272 | if ($new_record) { 273 | return $controller->redirect($this->Link()); 274 | } else if ($this->gridField->getList()->byId($this->record->ID)) { 275 | return $controller->redirectBack(); 276 | } else { 277 | // Changes to the record properties might've excluded the record from 278 | // a filtered list, so return back to the main view if it can't be found 279 | $noActionURL = $controller->removeAction($data['url']); 280 | $controller->getRequest()->addHeader('X-Pjax', 'Content'); 281 | return $controller->redirect($noActionURL, 302); 282 | } 283 | } 284 | 285 | public function doDelete($data, $form) 286 | { 287 | $title = $this->record->Title; 288 | try { 289 | if (!$this->record->canDelete()) { 290 | throw new ValidationException(_t('GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions"), 0); 291 | } 292 | 293 | $this->record->delete(); 294 | } catch (ValidationException $e) { 295 | $form->sessionMessage($e->getResult()->message(), ValidationResult::TYPE_ERROR); 296 | return Controller::curr()->redirectBack(); 297 | } 298 | 299 | $message = sprintf(_t('GridFieldDetailForm.Deleted', 'Deleted %s %s'), $this->record->i18n_singular_name(), htmlspecialchars($title, ENT_QUOTES)); 300 | 301 | $toplevelController = $this->getToplevelController(); 302 | if ($toplevelController && $toplevelController instanceof LeftAndMain) { 303 | $backForm = $toplevelController->getEditForm(); 304 | $backForm->sessionMessage($message, ValidationResult::TYPE_GOOD); 305 | } else { 306 | $form->sessionMessage($message, ValidationResult::TYPE_GOOD); 307 | } 308 | 309 | 310 | //Remove all requirements 311 | Requirements::clear(); 312 | 313 | return $this->customise(['GridFieldID' => $this->gridField->ID()])->renderWith(GridField::class . '_deleted'); 314 | } 315 | 316 | /** 317 | * Wrapper for redirectBack() 318 | * @see Controller::redirectBack() 319 | */ 320 | public function redirectBack(): HTTPResponse 321 | { 322 | return Controller::curr()->redirectBack(); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /templates/WebbuildersGroup/FrontEndGridField/Forms/GridField/GridFieldDetailForm.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $Title.XML 5 | <% base_tag %> 6 | 7 | <% require themedCSS(layout) %> 8 | <% require themedCSS(typography) %> 9 | <% require themedCSS(form) %> 10 | 11 | 12 |

$Title.XML

13 | 14 | $ItemEditForm 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/WebbuildersGroup/FrontEndGridField/Forms/GridField/GridField_deleted.ss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $Title.XML 5 | 6 | 7 | 14 | 15 | --------------------------------------------------------------------------------