├── resources ├── screenshots │ └── plugin_logo.png ├── icon.svg ├── icon-mask.svg └── js │ └── limitblocktype.js ├── composer.json ├── controllers └── LimitBlockTypeController.php ├── releases.json ├── .gitignore ├── LICENSE.txt ├── records └── LimitBlockTypeRecord.php ├── README.md ├── services └── LimitBlockTypeService.php └── LimitBlockTypePlugin.php /resources/screenshots/plugin_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmikkel/limitblocktype/HEAD/resources/screenshots/plugin_logo.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmikkel/limitblocktype", 3 | "description": "Adds option to limit individual Matrix block types", 4 | "type": "craft-plugin", 5 | "authors": [ 6 | { 7 | "name": "Mats Mikkel Rummelhoff", 8 | "homepage": "http://mmikkel.no" 9 | } 10 | ], 11 | "require": { 12 | "composer/installers": "~1.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /controllers/LimitBlockTypeController.php: -------------------------------------------------------------------------------- 1 | limitBlockType->saveBlockTypeLimitsFromPost($object); 22 | parent::redirectToPostedUrl($object, $default); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /releases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "1.0.2", 4 | "downloadUrl": "https://github.com/mmikkel/limitblocktype/archive/master.zip", 5 | "date": "2017-08-05T10:00:54.326Z", 6 | "notes": [ 7 | "[Fixed] Fixes an issue where Limit Block Type would break password resets (undefined index)" 8 | ] 9 | }, 10 | { 11 | "version": "1.0.1", 12 | "downloadUrl": "https://github.com/mmikkel/limitblocktype/archive/master.zip", 13 | "date": "2016-09-05T10:00:54.326Z", 14 | "notes": [ 15 | "[Fixed] Fixed a bug" 16 | ] 17 | }, 18 | { 19 | "version": "1.0.0", 20 | "downloadUrl": "https://github.com/mmikkel/limitblocktype/archive/master.zip", 21 | "date": "2016-09-03T11:20:54.326Z", 22 | "notes": [ 23 | "[Added] Initial release" 24 | ] 25 | } 26 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | !.gitignore 5 | 6 | .ssh/ 7 | _backup/ 8 | 9 | *.sassc 10 | .sass-cache 11 | .DS_Store 12 | 13 | db-sync 14 | tmp 15 | 16 | # Compiled source # 17 | ################### 18 | *.com 19 | *.class 20 | *.dll 21 | *.exe 22 | *.o 23 | *.so 24 | */.sass-cache/* 25 | 26 | # Packages # 27 | ############ 28 | # it's better to unpack these files and commit the raw source 29 | # git has its own built in compression methods 30 | *.7z 31 | *.dmg 32 | *.gz 33 | *.iso 34 | *.jar 35 | *.rar 36 | *.tar 37 | *.zip 38 | *.sublime-project 39 | *.sublime-workspace 40 | 41 | # Logs and databases # 42 | ###################### 43 | *.log 44 | 45 | # OS generated files # 46 | ###################### 47 | .DS_Store 48 | .DS_Store? 49 | ._* 50 | .Spotlight-V100 51 | .Trashes 52 | Icon? 53 | ehthumbs.db 54 | Thumbs.db 55 | 56 | 57 | # Project # 58 | ###################### 59 | 60 | node_modules/ 61 | bower_components/ 62 | /vendor/ 63 | /.idea -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Mats Mikkel Rummelhoff 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /records/LimitBlockTypeRecord.php: -------------------------------------------------------------------------------- 1 | AttributeType::Number, 34 | 'typeId' => AttributeType::Number, 35 | 'limit' => AttributeType::Number 36 | ); 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | public function defineRelations() 43 | { 44 | return array( 45 | 'field' => array( 46 | static::BELONGS_TO, 47 | 'FieldRecord', 48 | 'fieldId', 49 | 'onDelete' => static::CASCADE, 50 | ), 51 | 'matrixBlockType' => array( 52 | static::BELONGS_TO, 53 | 'MatrixBlockTypeRecord', 54 | 'typeId', 55 | 'onDelete' => static::CASCADE, 56 | ), 57 | ); 58 | } 59 | } -------------------------------------------------------------------------------- /resources/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 14 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /resources/icon-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 14 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Limit Block Type plugin 1.0.2 for Craft CMS ![Craft 2.6](https://img.shields.io/badge/craft-2.6-green.svg?style=flat-square) 2 | 3 | Craft CMS plugin adding option to limit individual Matrix block types. 4 | 5 | ![Adding a maximum number of blocks to individual block types](http://g.recordit.co/AFk5hOEwzu.gif) 6 | 7 | ## What? 8 | 9 | Matrix is lacking an important feature – the ability to limit blocks per block type. 10 | This tiny plugin (which was surprisingly difficult to build) fixes that. 11 | 12 | Limit Block Type works with your existing Matrix fields. 13 | 14 | ## Usage 15 | 16 | Add the desired number of max blocks to your each of your block types’ settings (by clicking the block type settings’ cogwheel inside the Matrix configurator). Profit. 17 | 18 | ## Caution 19 | 20 | Limit Block Type is basically one huge hack, and could break at any time (for instance, if P&T redesigns the Craft Control Panel). The plugin is designed to die gracefully and not kill your editing experience if it ever stops working, but please keep this in mind. 21 | 22 | Also, if P&T ever adds this feature to the core, this plugin will immediately be retired. +1 for core feature, though – [please vote for the feature request](https://craftcms.uservoice.com/forums/285221-feature-requests/suggestions/7192758-limit-matrix-blocks-per-block-type)! 23 | 24 | ## Installation 25 | 26 | To install Limit Block Type, follow these steps: 27 | 28 | 1. Download & unzip the file and place the `limitblocktype` directory into your `craft/plugins` directory 29 | 2. -OR- do a `git clone https://github.com/mmikkel/limitblocktype.git` directly into your `craft/plugins` folder. You can then update it with `git pull` 30 | 4. Install plugin in the Craft Control Panel under Settings > Plugins 31 | 5. The plugin folder should be named `limitblocktype` for Craft to see it. GitHub recently started appending `-master` (the branch name) to the name of the folder for zip file downloads. 32 | 33 | **Limit Block Type requires Craft 2.6.x or newer.** 34 | 35 | ## Disclaimer, support 36 | 37 | This plugin is provided free of charge and you can do whatever you want with it. Limit Block Type is unlikely to mess up your stuff, but just to be clear: the author is not responsible for data loss or any other problems resulting from the use of this plugin. 38 | 39 | Please report any bugs, feature requests or other issues [here](https://github.com/mmikkel/limitblocktype/issues). Note that this is a hobby project and no promises are made regarding response time, feature implementations or bug fixes. 40 | 41 | 42 | ## Limit Block Type Changelog 43 | 44 | ### 1.0.2 –– 2017.08.31 45 | 46 | * Fixes an issue where Limit Block Type would break password resets (undefined index) 47 | 48 | ### 1.0.1 -- 2016.09.03 49 | 50 | * Fixed a bug 51 | 52 | ### 1.0.0 -- 2016.09.03 53 | 54 | * Initial release 55 | 56 | Brought to you by [Mats Mikkel Rummelhoff](http://mmikkel.no) 57 | -------------------------------------------------------------------------------- /services/LimitBlockTypeService.php: -------------------------------------------------------------------------------- 1 | config->get('devMode') ? craft()->fileCache->get($this->cacheKey) : null; 25 | 26 | if (!$data) { 27 | 28 | $records = LimitBlockTypeRecord::model()->findAll(); 29 | $data = []; 30 | 31 | $matrixFieldHandles = []; 32 | $matrixBlockHandles = []; 33 | 34 | foreach ($records as $record) { 35 | 36 | $fieldId = $record->fieldId; 37 | $typeId = $record->typeId; 38 | $limit = $record->limit ? (int) $record->limit : null; 39 | 40 | if (!isset($matrixFieldHandles[$fieldId])) { 41 | $matrixField = craft()->fields->getFieldById($fieldId); 42 | $matrixFieldHandles[$fieldId] = $matrixField->handle; 43 | $matrixBlocks = craft()->matrix->getBlockTypesByFieldId($fieldId); 44 | foreach ($matrixBlocks as $matrixBlock) { 45 | $matrixBlockHandles[$matrixBlock->id] = $matrixBlock->handle; 46 | } 47 | } 48 | 49 | $matrixFieldHandle = $matrixFieldHandles[$fieldId].':'.$fieldId; 50 | 51 | if (!isset($data[$matrixFieldHandle])) { 52 | $data[$matrixFieldHandle] = []; 53 | } 54 | 55 | $matrixBlockHandle = $matrixBlockHandles[$typeId].':'.$typeId; 56 | 57 | $data[$matrixFieldHandle][$matrixBlockHandle] = $limit; 58 | 59 | } 60 | 61 | craft()->fileCache->set($this->cacheKey, $data, 1800); // Cache for 30 minutes 62 | 63 | } 64 | 65 | return $data; 66 | 67 | } 68 | 69 | public function saveBlockTypeLimitsFromPost(FieldModel $field) 70 | { 71 | 72 | $vars = craft()->request->getPost(); 73 | 74 | $matrixFieldBlockTypes = craft()->matrix->getBlockTypesByFieldId($field->id); 75 | $postedBlockTypes = $vars['types']['Matrix']['blockTypes'] ?: null; 76 | 77 | if (!$postedBlockTypes || empty($postedBlockTypes) || !$matrixFieldBlockTypes || empty($matrixFieldBlockTypes)) { 78 | return false; 79 | } 80 | 81 | $postedBlockTypesByHandle = []; 82 | 83 | foreach ($postedBlockTypes as $blockType) { 84 | $postedBlockTypesByHandle[$blockType['handle']] = $blockType; 85 | } 86 | 87 | foreach ($matrixFieldBlockTypes as $blockType) { 88 | $limit = @$postedBlockTypesByHandle[$blockType->handle]['limit'] ?: null; 89 | $this->saveLimitForBlockType($blockType, $field, $limit); 90 | } 91 | 92 | } 93 | 94 | public function saveLimitForBlockType(MatrixBlockTypeModel $blockType, FieldModel $field, $limit) 95 | { 96 | 97 | $attributes = [ 98 | 'fieldId' => $field->id, 99 | 'typeId' => $blockType->id, 100 | ]; 101 | 102 | $record = LimitBlockTypeRecord::model()->findByAttributes($attributes); 103 | 104 | if (!$record) { 105 | $record = new LimitBlockTypeRecord(); 106 | } 107 | 108 | $attributes['limit'] = $limit; 109 | 110 | $record->setAttributes($attributes); 111 | 112 | //$record->validate(); 113 | 114 | // TODO: Create a model to validate errors on 115 | 116 | $transaction = craft()->db->getCurrentTransaction() === null ? craft()->db->beginTransaction() : null; 117 | 118 | try { 119 | 120 | if (!$record->id) { 121 | $record->save(); 122 | } else { 123 | $record->update(); 124 | } 125 | 126 | if ($transaction !== null) { 127 | $transaction->commit(); 128 | } 129 | 130 | } catch (\Exception $error) { 131 | 132 | if ($transaction !== null) { 133 | $transaction->rollback(); 134 | } 135 | 136 | throw $error; 137 | 138 | } 139 | 140 | craft()->fileCache->delete($this->cacheKey); 141 | 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /LimitBlockTypePlugin.php: -------------------------------------------------------------------------------- 1 | request->isCpRequest() || craft()->isConsole()) { 37 | return false; 38 | } 39 | 40 | $isAjaxRequest = craft()->request->isAjaxRequest(); 41 | $segments = craft()->request->segments; 42 | $actionSegment = array_pop($segments) ?: ''; 43 | 44 | if (!$isAjaxRequest) { 45 | 46 | craft()->templates->includeJsResource('limitblocktype/js/limitblocktype.js'); 47 | 48 | // Check request parameters 49 | $vars = craft()->request->getPost(); 50 | if (!empty($vars) && isset($vars['action']) && $vars['action'] === 'fields/saveField' && $vars['type'] === 'Matrix') { 51 | // This is so hacky I don't even 52 | craft()->runController('limitBlockType/saveField'); 53 | } 54 | 55 | } 56 | 57 | if (!$isAjaxRequest || $actionSegment === 'getEditorHtml') { 58 | $data = JsonHelper::encode(craft()->limitBlockType->getData()); 59 | craft()->templates->includeJs('if (Craft && Craft.LimitBlockTypePlugin) { new Craft.LimitBlockTypePlugin('.$data.', '.($isAjaxRequest ? '1' : '0').'); }'); 60 | } 61 | 62 | } 63 | 64 | /** 65 | * @return mixed 66 | */ 67 | public function getName() 68 | { 69 | return Craft::t($this->_pluginName); 70 | } 71 | 72 | /** 73 | * @return mixed 74 | */ 75 | public function getDescription() 76 | { 77 | return Craft::t($this->_description); 78 | } 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function getPluginUrl() 84 | { 85 | return $this->_pluginUrl; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getDocumentationUrl() 92 | { 93 | return $this->_documentationUrl; 94 | } 95 | 96 | /** 97 | * @return string 98 | */ 99 | public function getReleaseFeedUrl() 100 | { 101 | return $this->_releaseFeedUrl; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getVersion() 108 | { 109 | return $this->_version; 110 | } 111 | 112 | /** 113 | * @return string 114 | */ 115 | public function getSchemaVersion() 116 | { 117 | return $this->_schemaVersion; 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function getDeveloper() 124 | { 125 | return $this->_developer; 126 | } 127 | 128 | /** 129 | * @return string 130 | */ 131 | public function getDeveloperUrl() 132 | { 133 | return $this->_developerUrl; 134 | } 135 | 136 | /** 137 | * @return bool 138 | */ 139 | public function hasCpSection() 140 | { 141 | return false; 142 | } 143 | 144 | /** 145 | */ 146 | public function onBeforeInstall() 147 | { 148 | if (!$this->isCraftRequiredVersion()) { 149 | craft()->userSession->setError(Craft::t('{pluginName} requires Craft {minVersion} or newer, and was not installed.', array( 150 | 'pluginName' => $this->getName(), 151 | 'minVersion' => $this->getCraftRequiredVersion(), 152 | ))); 153 | return false; 154 | } 155 | } 156 | 157 | /** 158 | */ 159 | public function onAfterInstall() 160 | { 161 | } 162 | 163 | /** 164 | */ 165 | public function onBeforeUninstall() 166 | { 167 | } 168 | 169 | /** 170 | */ 171 | public function onAfterUninstall() 172 | { 173 | } 174 | 175 | /** 176 | * @return string 177 | */ 178 | public function getCraftRequiredVersion() 179 | { 180 | return $this->_minVersion; 181 | } 182 | 183 | /** 184 | * @return mixed 185 | */ 186 | public function isCraftRequiredVersion() 187 | { 188 | return version_compare(craft()->getVersion(), $this->getCraftRequiredVersion(), '>='); 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /resources/js/limitblocktype.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Limit Block Type plugin 3 | * 4 | */ 5 | Craft.LimitBlockTypePlugin = Garnish.Base.extend({ 6 | 7 | init: function(data, isAjaxRequest) { 8 | 9 | if (Craft.MatrixConfigurator) { 10 | new Craft.LimitBlockTypePlugin_Configurator(data, isAjaxRequest); 11 | } 12 | if (Craft.MatrixInput) { 13 | new Craft.LimitBlockTypePlugin_Input(data, isAjaxRequest); 14 | } 15 | } 16 | 17 | }); 18 | 19 | Craft.LimitBlockTypePlugin_Configurator = Garnish.Base.extend({ 20 | 21 | data: null, 22 | configurator: null, 23 | 24 | init: function (data) { 25 | 26 | this.data = this.getData(data || {}); 27 | 28 | var self = this; 29 | 30 | var init = Craft.MatrixConfigurator.prototype.init; 31 | Craft.MatrixConfigurator.prototype.init = function () { 32 | init.apply(this, arguments); 33 | self.configurator = this; 34 | self.patchBlockTypes(this.blockTypes); 35 | } 36 | 37 | // Add the "Max blocks" setting to the block type settings modal 38 | var getBlockTypeSettingsModal = Craft.MatrixConfigurator.prototype.getBlockTypeSettingsModal; 39 | Craft.MatrixConfigurator.prototype.getBlockTypeSettingsModal = function () { 40 | 41 | if (!this.blockTypeSettingsModal) { 42 | 43 | var blockTypeSettingsModal = getBlockTypeSettingsModal.apply(this, arguments); 44 | blockTypeSettingsModal.$limitField = $('
'); 45 | blockTypeSettingsModal.$limitHeading = $('
').appendTo(blockTypeSettingsModal.$limitField); 46 | blockTypeSettingsModal.$limitLabel = $('').appendTo(blockTypeSettingsModal.$limitHeading); 47 | blockTypeSettingsModal.$limitInstructions = $('

'+Craft.t('The maximum number of blocks allowed for this block type')+'

').appendTo(blockTypeSettingsModal.$limitHeading); 48 | blockTypeSettingsModal.$limitInputContainer = $('
').appendTo(blockTypeSettingsModal.$limitField); 49 | blockTypeSettingsModal.$limitInput = $('').appendTo(blockTypeSettingsModal.$limitInputContainer); 50 | blockTypeSettingsModal.$handleField.after(blockTypeSettingsModal.$limitField); 51 | 52 | var show = blockTypeSettingsModal.show; 53 | blockTypeSettingsModal.show = function (name, handle, limit, errors) { 54 | this.$limitInput.val(typeof limit == 'string' ? limit : ''); 55 | show.apply(this, arguments); 56 | } 57 | 58 | blockTypeSettingsModal.onFormSubmit = function(ev) 59 | { 60 | ev.preventDefault(); 61 | 62 | // Prevent multi form submits with the return key 63 | if (!this.visible) 64 | { 65 | return; 66 | } 67 | 68 | if (this.handleGenerator.listening) 69 | { 70 | // Give the handle a chance to catch up with the input 71 | this.handleGenerator.updateTarget(); 72 | } 73 | 74 | // Basic validation 75 | var name = Craft.trim(this.$nameInput.val()), 76 | handle = Craft.trim(this.$handleInput.val()), 77 | limit = Craft.trim(this.$limitInput.val()); 78 | 79 | if (!name || !handle) 80 | { 81 | Garnish.shake(this.$form); 82 | } 83 | else 84 | { 85 | this.hide(); 86 | this.onSubmit(name, handle, limit); 87 | } 88 | }; 89 | blockTypeSettingsModal.removeListener(blockTypeSettingsModal.$form, 'submit'); 90 | blockTypeSettingsModal.addListener(blockTypeSettingsModal.$form, 'submit', 'onFormSubmit'); 91 | this.blockTypeSettingsModal = blockTypeSettingsModal; 92 | } 93 | return this.blockTypeSettingsModal; 94 | } 95 | 96 | /* 97 | * For the new blocks on the block 98 | * 99 | */ 100 | var addBlockType = Craft.MatrixConfigurator.prototype.addBlockType; 101 | var self = this; 102 | Craft.MatrixConfigurator.prototype.addBlockType = function () { 103 | addBlockType.apply(this, arguments); 104 | var superSubmit = this.blockTypeSettingsModal.onSubmit; 105 | this.blockTypeSettingsModal.onSubmit = (function (name, handle) { 106 | var limit = parseInt(this.$limitInput.val()) || null; 107 | superSubmit(name, handle); 108 | self.patchBlockTypes(self.configurator.blockTypes); 109 | var newBlockId = 'new'+self.configurator.totalNewBlockTypes; 110 | self.configurator.blockTypes[newBlockId].applySettings(name, handle, limit); 111 | self.setData({blockId: newBlockId, limit: limit}); 112 | }).bind(this.blockTypeSettingsModal); 113 | } 114 | 115 | }, 116 | 117 | getData: function (data) { 118 | // For the configurator, we just need the block ids and limits 119 | var temp = {}; 120 | for (var i in data) { 121 | for (var j in data[i]) { 122 | temp[j.split(':')[1]] = data[i][j]; 123 | } 124 | } 125 | return $.extend(temp, Craft.getLocalStorage('limitblocktypesdata') || {}); 126 | }, 127 | 128 | // Store in localStorage, to retain temporary values when the form fails to submit 129 | setData: function (data) { 130 | var blockId = data.blockId || null; 131 | var limit = data.limit || null; 132 | if (blockId === null || limit === null) { 133 | return false; 134 | } 135 | this.data[blockId] = limit; 136 | Craft.setLocalStorage('limitblocktypesdata', this.data); 137 | }, 138 | 139 | patchBlockTypes: function(blockTypes) { 140 | 141 | for (var id in blockTypes) { 142 | 143 | var blockType = blockTypes[id]; 144 | var self = this; 145 | 146 | if (!blockType.$limitHiddenInput) { 147 | 148 | blockType.$limitHiddenInput = $(''); 149 | blockType.$handleHiddenInput.after(blockType.$limitHiddenInput); 150 | 151 | var superApplySettings = blockType.applySettings; 152 | blockType.applySettings = function (name, handle, limit) { 153 | limit = parseInt(limit) || null; 154 | this.$limitHiddenInput.val(limit); 155 | self.setData({blockId: this.id, limit: limit}); 156 | superApplySettings.apply(this, arguments); 157 | }; 158 | 159 | blockType.showSettings = (function () { 160 | var blockTypeSettingsModal = this.configurator.getBlockTypeSettingsModal(); 161 | blockTypeSettingsModal.show(this.$nameHiddenInput.val(), this.$handleHiddenInput.val(), this.$limitHiddenInput.val(), this.errors); 162 | blockTypeSettingsModal.onSubmit = this.applySettings.bind(this); 163 | blockTypeSettingsModal.onDelete = $.proxy(this, 'selfDestruct'); 164 | }).bind(blockType); 165 | 166 | blockType.removeListener(blockType.$settingsBtn, 'click'); 167 | blockType.addListener(blockType.$settingsBtn, 'click', 'showSettings'); 168 | 169 | this.configurator.blockTypes[id] = blockType; 170 | 171 | } 172 | } 173 | 174 | }, 175 | 176 | getLimitForBlockTypeId: function (blockTypeId) { 177 | return this.data[blockTypeId] || null; 178 | } 179 | 180 | }); 181 | 182 | Craft.LimitBlockTypePlugin_Input = Garnish.Base.extend({ 183 | 184 | $context: null, 185 | data: null, 186 | fields: {}, 187 | disabledBlockTypes: [], 188 | 189 | init: function (data, isAjaxRequest) { 190 | 191 | if (isAjaxRequest) { 192 | this.context = '.body.elementeditor:last'; 193 | } else { 194 | this.context = '#fields'; 195 | } 196 | 197 | this.data = data || {}; 198 | 199 | var self = this; 200 | var init = Craft.MatrixInput.prototype.init; 201 | 202 | Craft.MatrixInput.prototype.init = function () { 203 | 204 | init.apply(this, arguments); 205 | 206 | var fieldHandle = this.id.split('-').pop(); 207 | var fieldData = self.getFieldDataByHandle(fieldHandle); 208 | var blockData = {}; 209 | 210 | for (var i in fieldData) { 211 | blockData[i.split(':')[0]] = { 212 | id: i.split(':')[1], 213 | limit: fieldData[i] 214 | }; 215 | } 216 | 217 | self.fields[fieldHandle] = { 218 | instance: this, 219 | blockData: blockData 220 | }; 221 | 222 | this.removeListener(this.$addBlockBtnGroupBtns, 'click'); 223 | this.addListener(this.$addBlockBtnGroupBtns, 'click', function(ev) { 224 | var type = $(ev.target).data('type'); 225 | var fieldHandle = this.id.split('-').pop(); 226 | this.addBlock(type, fieldHandle); 227 | }); 228 | 229 | self.patchField(fieldHandle); 230 | 231 | } 232 | 233 | var addBlock = Craft.MatrixInput.prototype.addBlock; 234 | Craft.MatrixInput.prototype.addBlock = function (type, $insertBefore) { 235 | var fieldHandle; 236 | var args; 237 | if (typeof $insertBefore == 'string') { 238 | fieldHandle = $insertBefore; 239 | args = [arguments[0]] 240 | } else if ($insertBefore && $insertBefore.length) { 241 | fieldHandle = $insertBefore.closest('.matrix.matrix-field').attr('id').split('-')[1] || null; 242 | args = arguments; 243 | } else { 244 | fieldHandle = self.activeMatrixFieldHandle || null; 245 | args = [arguments[0]]; 246 | } 247 | if (fieldHandle) { 248 | // Is this block type limited for this field? 249 | var fieldData = self.fields[fieldHandle] || null; 250 | if (fieldData && $.inArray(type, (fieldData.disabledBlockTypes||[])) >= 0) { 251 | return false; 252 | } 253 | } 254 | addBlock.apply(this, args); 255 | self.patchField(fieldHandle); 256 | } 257 | 258 | var updateAddBlockBtn = Craft.MatrixInput.prototype.updateAddBlockBtn; 259 | Craft.MatrixInput.prototype.updateAddBlockBtn = function () { 260 | updateAddBlockBtn.apply(this, arguments); 261 | Garnish.requestAnimationFrame((function () { 262 | this.patchFields(); 263 | }).bind(self)); 264 | } 265 | 266 | var setNewBlockBtn = Craft.MatrixInput.prototype.setNewBlockBtn; 267 | Craft.MatrixInput.prototype.setNewBlockBtn = function () { 268 | setNewBlockBtn.apply(this, arguments); 269 | Garnish.requestAnimationFrame((function () { 270 | this.patchFields(); 271 | }).bind(self)); 272 | } 273 | 274 | $('body').on('click', '.matrix.matrix-field', (function (e) { 275 | this.activeMatrixFieldHandle = $(e.currentTarget).attr('id').split('-').pop(); 276 | }).bind(this)); 277 | $('body').on('click', '.matrixblock .actions a.settings', this.onBlockSettingsBtnClick.bind(this)); 278 | $('body').on('click', '.matrix.matrix-field .buttons .btn.add', this.onBlockSettingsBtnClick.bind(this)); 279 | 280 | }, 281 | 282 | getFieldDataByHandle: function (handle) { 283 | for (var i in this.data) { 284 | if (i.split(':')[0] === handle) { 285 | return this.data[i]; 286 | } 287 | } 288 | return null; 289 | }, 290 | 291 | onBlockSettingsBtnClick: function (e) { 292 | $('.menu:last a[data-type]').removeClass('disabled'); 293 | Garnish.requestAnimationFrame((function () { 294 | this.patchFields(); 295 | }).bind(this)); 296 | }, 297 | 298 | patchFields: function () { 299 | if (this.activeMatrixFieldHandle) { 300 | this.patchField(this.activeMatrixFieldHandle); 301 | } else { 302 | for (var fieldHandle in this.fields) { 303 | this.patchField(fieldHandle); 304 | } 305 | } 306 | }, 307 | 308 | patchField: function (fieldHandle) { 309 | 310 | if (!this.fields[fieldHandle]) { 311 | return false; 312 | } 313 | 314 | var self = this; 315 | var instance = this.fields[fieldHandle].instance; 316 | var blockData = this.fields[fieldHandle].blockData; 317 | var $field = instance.$container; 318 | var $addBlockMenuBtn = instance.$addBlockMenuBtn; 319 | var $addBlockBtnGroupBtns = instance.$addBlockBtnGroupBtns; 320 | 321 | this.fields[fieldHandle].disabledBlockTypes = []; 322 | 323 | $addBlockBtnGroupBtns.each(function () { 324 | 325 | var $btn = $(this); 326 | var blockTypeHandle = $btn.data('type'); 327 | 328 | if (!blockData[blockTypeHandle]) { 329 | return; 330 | } 331 | 332 | var blockTypeId = blockData[blockTypeHandle].id; 333 | var blockTypeLimit = blockData[blockTypeHandle].limit || null; 334 | var blockTypeCount = $field.find('.matrixblock[data-id] > input[type="hidden"][value="'+blockTypeHandle+'"]').length; 335 | 336 | var $menuBtn = $('.menu:last a[data-type="'+blockTypeHandle+'"]'); 337 | 338 | if (blockTypeLimit && blockTypeCount >= blockTypeLimit) { 339 | $btn.addClass('disabled'); 340 | if ($menuBtn.length) { 341 | $menuBtn.addClass('disabled'); 342 | } 343 | self.fields[fieldHandle].disabledBlockTypes.push(blockTypeHandle); 344 | } else { 345 | $btn.removeClass('disabled'); 346 | if ($menuBtn.length) { 347 | $menuBtn.removeClass('disabled'); 348 | } 349 | } 350 | }); 351 | 352 | }, 353 | 354 | getLimitForBlockTypeHandle: function (blockTypeHandle) { 355 | var limit = null; 356 | for (var i in this.data) { 357 | if (this.data[i].handle === blockTypeHandle) { 358 | return this.data[i].limit || null; 359 | } 360 | } 361 | return null; 362 | }, 363 | 364 | getBlockTypeIdByHandle: function (blockTypeHandle) { 365 | for (var id in this.data) { 366 | if (this.data[id].handle === blockTypeHandle) { 367 | return id; 368 | } 369 | } 370 | return null; 371 | } 372 | 373 | }); 374 | 375 | $(function () { 376 | if (!$('input[type="hidden"][name="action"][value="fields/saveField"]').length) { 377 | Craft.setLocalStorage('limitblocktypesdata', null); 378 | } 379 | }); --------------------------------------------------------------------------------