diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ee4b1e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 LoveOrigami + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..352a01f --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# yii2-modal-ajax + +A wrapper around Yii2 Bootstrap Modal for using an ActiveForm via AJAX inside. + +## Installation +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run +```sh +$ php composer.phar require --prefer-dist loveorigami/yii2-modal-ajax "@dev" +``` +or add +``` +"loveorigami/yii2-modal-ajax": "@dev" +``` +to the require section of your composer.json file. + +## Usage + +### Controller +Extend your basic logic with ajax render and save capabilities: +```php +public function actionCreate() +{ + $model = new Company(); + + if ($model->load(Yii::$app->request->post())) { + if ($model->save()) { + return $this->redirect(['view', 'id' => $model->id]); + } + } + + return $this->render('create', [ + 'model' => $model, + ]); +} +``` +to +```php +public function actionCreate() +{ + $model = new Company(); + + if ($model->load(Yii::$app->request->post())) { + if ($model->save()) { + if (Yii::$app->request->isAjax) { + // JSON response is expected in case of successful save + Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + return ['success' => true]; + } + return $this->redirect(['view', 'id' => $model->id]); + } + } + + if (Yii::$app->request->isAjax) { + return $this->renderAjax('create', [ + 'model' => $model, + ]); + } else { + return $this->render('create', [ + 'model' => $model, + ]); + } +} +``` + + +### View +```php +use lo\widgets\modal\Modal; + +Modal::begin([ + 'id' => 'createCompany', + 'url' => Url::to(['/partner/default/create']), // Ajax view with form to load + 'ajaxSubmit' => true, // Submit the contained form as ajax, true by default + // ... any other yii2 bootstrap modal option you need +]); +Modal::end(); +``` + +## Plugin Events + +On top if the basic twitter bootstrap modal events the following events are triggered + + +### `kbModalBeforeShow` +This event is triggered right before the view for the form is loaded. Additional parameters available with this event are: +- `xhr`: _XMLHttpRequest_, the jQuery XML Http Request object used for this transaction. +- `settings`: _object_, the jQuery ajax settings for this transaction. + +```js +$('#createCompany').on('kbModalBeforeShow', function(event, xhr, settings) { + console.log('kbModalBeforeShow'); +}); +``` + +### `kbModalShow` +This event is triggered after the view for the form is successful loaded. Additional parameters available with this event are: +- `data`: _object_, the data object sent via server's response. +- `status`: _string_, the jQuery AJAX success text status. +- `xhr`: _XMLHttpRequest_, the jQuery XML Http Request object used for this transaction. + +```js +$('#createCompany').on('kbModalShow', function(event, data, status, xhr) { + console.log('kbModalShow'); +}); +``` + +### `kbModalBeforeSubmit` +This event is triggered right before the form is submitted. Additional parameters available with this event are: +- `xhr`: _XMLHttpRequest_, the jQuery XML Http Request object used for this transaction. +- `settings`: _object_, the jQuery ajax settings for this transaction. + +```js +$('#createCompany').on('kbModalBeforeSubmit', function(event, xhr, settings) { + console.log('kbModalBeforeSubmit'); +}); +``` + +### `kbModalSubmit` +This event is triggered after the form is successful submitted. Additional parameters available with this event are: +- `data`: _object_, the data object sent via server's response. +- `status`: _string_, the jQuery AJAX success text status. +- `xhr`: _XMLHttpRequest_, the jQuery XML Http Request object used for this transaction. + +```js +$('#createCompany').on('kbModalSubmit', function(event, data, status, xhr) { + console.log('kbModalSubmit'); + // You may call pjax reloads here +}); +``` diff --git a/assets/js/kb-modal-ajax.js b/assets/js/kb-modal-ajax.js new file mode 100644 index 0000000..d6c7afa --- /dev/null +++ b/assets/js/kb-modal-ajax.js @@ -0,0 +1,176 @@ +(function($) { + "use strict"; + + var pluginName = 'kbModalAjax'; + + /** + * Retrieves the script tags in document + * @return {Array} + */ + var getPageScriptTags = function () { + var scripts = []; + jQuery('script[src]').each(function () { + scripts.push(jQuery(this).attr('src')); + }); + return scripts; + }; + + + /** + * Retrieves the CSS links in document + * @return {Array} + */ + var getPageCssLinks = function () { + var links = []; + jQuery('link[rel="stylesheet"]').each(function () { + links.push(jQuery(this).attr('href')); + }); + return links; + }; + + function ModalAjax(element, options) { + this.element = element; + this.init(options); + }; + + ModalAjax.prototype.init = function(options) { + this.initalRequestUrl = options.url; + this.ajaxSubmit = options.ajaxSubmit || true; + jQuery(this.element).on('show.bs.modal', this.shown.bind(this)); + }; + + /** + * Requests the content of the modal and injects it, called after the + * modal is shown + */ + ModalAjax.prototype.shown = function() { + // Clear original html before loading + jQuery(this.element).find('.modal-body').html(''); + + jQuery.ajax({ + url: this.initalRequestUrl, + context: this, + beforeSend: function (xhr, settings) { + jQuery(this.element).triggerHandler('kbModalBeforeShow', [xhr, settings]); + }, + success: function(data, status, xhr) { + this.injectHtml(data); + if (this.ajaxSubmit) { + jQuery(this.element).off('submit').on('submit', this.formSubmit.bind(this)); + } + jQuery(this.element).triggerHandler('kbModalShow', [data, status, xhr]); + } + }); + }; + + /** + * Injects the form of given html into the modal and extecutes css and js + * @param {string} html the html to inject + */ + ModalAjax.prototype.injectHtml = function(html) { + // Find form and inject it + var form = jQuery(html).filter('form'); + + // Remove existing forms + if (jQuery(this.element).find('form').length > 0) { + jQuery(this.element).find('form').off().yiiActiveForm('destroy').remove(); + } + + jQuery(this.element).find('.modal-body').html(form); + + var knownScripts = getPageScriptTags(); + var knownCssLinks = getPageCssLinks(); + var newScripts = []; + var inlineInjections = []; + var loadedScriptsCount = 0; + + // Find some element to append to + var headTag = jQuery('head'); + if (headTag.length < 1) { + headTag = jQuery('body'); + if (headTag.length < 1) { + headTag = jQuery(document); + } + } + + // CSS stylesheets that haven't been added need to be loaded + jQuery(html).filter('link[rel="stylesheet"]').each(function () { + var href = jQuery(this).attr('href'); + + if (knownCssLinks.indexOf(href) < 0) { + // Append the CSS link to the page + headTag.append(jQuery(this).prop('outerHTML')); + // Store the link so its not needed to be requested again + knownCssLinks.push(href); + } + }); + + // Scripts that haven't yet been loaded need to be added to the end of the body + jQuery(html).filter('script').each(function () { + var src = jQuery(this).attr("src"); + + if (typeof src === 'undefined') { + // If no src supplied, execute the raw JS (need to execute after the script tags have been loaded) + inlineInjections.push(jQuery(this).text()); + } else if (knownScripts.indexOf(src) < 0) { + // Prepare src so we can append GET parameter later + src += (src.indexOf('?') < 0) ? '?' : '&'; + newScripts.push(src); + } + }); + + /** + * Scripts loaded callback + */ + var scriptLoaded = function () { + loadedScriptsCount += 1; + if (loadedScriptsCount === newScripts.length) { + // Execute inline scripts + for (var i = 0; i < inlineInjections.length; i += 1) { + window.eval(inlineInjections[i]); + } + } + }; + + // Load each script tag + for (var i = 0; i < newScripts.length; i += 1) { + jQuery.getScript(newScripts[i] + (new Date().getTime()), scriptLoaded); + } + }; + + /** + * Adds event handlers to the form to check for submit + */ + ModalAjax.prototype.formSubmit = function() { + var form = jQuery(this.element).find('form'); + + // Convert form to ajax submit + jQuery.ajax({ + type: form.attr('method'), + url: form.attr('action'), + data: form.serialize(), + context: this, + beforeSend: function (xhr, settings) { + jQuery(this.element).triggerHandler('kbModalBeforeSubmit', [xhr, settings]); + }, + success: function(data, status, xhr) { + var contentType = xhr.getResponseHeader('content-type') || ''; + if (contentType.indexOf('html') > -1) { + // Assume form contains errors if html + this.injectHtml(data); + } + jQuery(this.element).triggerHandler('kbModalSubmit', [data, status, xhr]); + } + }); + + return false; + }; + + $.fn[pluginName] = function(options) { + return this.each(function () { + if (!$.data(this, pluginName)) { + $.data(this, pluginName, new ModalAjax(this, options)); + } + }); + }; +})(jQuery); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0ec4107 --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "loveorigami/yii2-modal-ajax", + "description": "A wrapper around Yii2 Bootstrap Modal for using an ActiveForm via AJAX inside", + "keywords": [ + "yii2", + "bootstrap", + "modal" + ], + "homepage": "https://github.com/loveorigami/yii2-modal-ajax", + "type": "yii2-extension", + "license": "MIT", + "authors": [ + { + "name": "Karnbrock Gmbh", + "homepage": "http://karnbrock.biz" + }, + { + "name": "Lukyanov Andrey", + "homepage": "http://loveorigami.info" + } + ], + "autoload": { + "psr-4": { + "lo\\widgets\\modal\\": "/src" + } + } +} diff --git a/src/Modal.php b/src/Modal.php new file mode 100644 index 0000000..74ad157 --- /dev/null +++ b/src/Modal.php @@ -0,0 +1,46 @@ + + */ +class Modal extends BaseModal +{ + /** + * The url to request when modal is opened + * @var string + */ + public $url; + + /** + * Submit the form via ajax + * @var boolean + */ + public $ajaxSubmit = true; + + /** + * @inheritdocs + */ + public function run() + { + $view = $this->getView(); + parent::run(); + + ModalAsset::register($view); + + $id = $this->options['id']; + $ajaxSubmit = $this->ajaxSubmit ? 'true' : 'false'; + $js = <<url}', + ajaxSubmit: {$ajaxSubmit}, + }); +JS; + $view->registerJs($js); + } +} diff --git a/src/ModalAsset.php b/src/ModalAsset.php new file mode 100644 index 0000000..30f6ecd --- /dev/null +++ b/src/ModalAsset.php @@ -0,0 +1,33 @@ + + */ +class ModalAsset extends AssetBundle +{ + /** + * @var string + */ + public $sourcePath = __DIR__ .'/assets'; + + /** + * @inheritdoc + */ + public $depends = [ + 'yii\bootstrap\BootstrapAsset', + ]; + + /** + * @inheritdoc + */ + public $js = [ + 'js/kb-modal-ajax.js', + ]; + +} diff --git a/src/assets/js/kb-modal-ajax.js b/src/assets/js/kb-modal-ajax.js new file mode 100644 index 0000000..64d20a6 --- /dev/null +++ b/src/assets/js/kb-modal-ajax.js @@ -0,0 +1,175 @@ +(function ($) { + "use strict"; + + var pluginName = 'kbModalAjax'; + + /** + * Retrieves the script tags in document + * @return {Array} + */ + var getPageScriptTags = function () { + var scripts = []; + jQuery('script[src]').each(function () { + scripts.push(jQuery(this).attr('src')); + }); + return scripts; + }; + + /** + * Retrieves the CSS links in document + * @return {Array} + */ + var getPageCssLinks = function () { + var links = []; + jQuery('link[rel="stylesheet"]').each(function () { + links.push(jQuery(this).attr('href')); + }); + return links; + }; + + function ModalAjax(element, options) { + this.element = element; + this.init(options); + } + + ModalAjax.prototype.init = function (options) { + this.initalRequestUrl = options.url; + this.ajaxSubmit = options.ajaxSubmit || true; + jQuery(this.element).on('show.bs.modal', this.shown.bind(this)); + }; + + /** + * Requests the content of the modal and injects it, called after the + * modal is shown + */ + ModalAjax.prototype.shown = function () { + // Clear original html before loading + jQuery(this.element).find('.modal-body').html(''); + + jQuery.ajax({ + url: this.initalRequestUrl, + context: this, + beforeSend: function (xhr, settings) { + jQuery(this.element).triggerHandler('kbModalBeforeShow', [xhr, settings]); + }, + success: function (data, status, xhr) { + this.injectHtml(data); + if (this.ajaxSubmit) { + jQuery(this.element).off('submit').on('submit', this.formSubmit.bind(this)); + } + jQuery(this.element).triggerHandler('kbModalShow', [data, status, xhr]); + } + }); + }; + + /** + * Injects the form of given html into the modal and extecutes css and js + * @param {string} html the html to inject + */ + ModalAjax.prototype.injectHtml = function (html) { + // Find form and inject it + var form = jQuery(html).filter('form'); + + // Remove existing forms + if (jQuery(this.element).find('form').length > 0) { + jQuery(this.element).find('form').off().yiiActiveForm('destroy').remove(); + } + + jQuery(this.element).find('.modal-body').html(form); + + var knownScripts = getPageScriptTags(); + var knownCssLinks = getPageCssLinks(); + var newScripts = []; + var inlineInjections = []; + var loadedScriptsCount = 0; + + // Find some element to append to + var headTag = jQuery('head'); + if (headTag.length < 1) { + headTag = jQuery('body'); + if (headTag.length < 1) { + headTag = jQuery(document); + } + } + + // CSS stylesheets that haven't been added need to be loaded + jQuery(html).filter('link[rel="stylesheet"]').each(function () { + var href = jQuery(this).attr('href'); + + if (knownCssLinks.indexOf(href) < 0) { + // Append the CSS link to the page + headTag.append(jQuery(this).prop('outerHTML')); + // Store the link so its not needed to be requested again + knownCssLinks.push(href); + } + }); + + // Scripts that haven't yet been loaded need to be added to the end of the body + jQuery(html).filter('script').each(function () { + var src = jQuery(this).attr("src"); + + if (typeof src === 'undefined') { + // If no src supplied, execute the raw JS (need to execute after the script tags have been loaded) + inlineInjections.push(jQuery(this).text()); + } else if (knownScripts.indexOf(src) < 0) { + // Prepare src so we can append GET parameter later + src += (src.indexOf('?') < 0) ? '?' : '&'; + newScripts.push(src); + } + }); + + /** + * Scripts loaded callback + */ + var scriptLoaded = function () { + loadedScriptsCount += 1; + if (loadedScriptsCount === newScripts.length) { + // Execute inline scripts + for (var i = 0; i < inlineInjections.length; i += 1) { + window.eval(inlineInjections[i]); + } + } + }; + + // Load each script tag + for (var i = 0; i < newScripts.length; i += 1) { + jQuery.getScript(newScripts[i] + (new Date().getTime()), scriptLoaded); + } + }; + + /** + * Adds event handlers to the form to check for submit + */ + ModalAjax.prototype.formSubmit = function () { + var form = jQuery(this.element).find('form'); + + // Convert form to ajax submit + jQuery.ajax({ + type: form.attr('method'), + url: form.attr('action'), + data: form.serialize(), + context: this, + beforeSend: function (xhr, settings) { + jQuery(this.element).triggerHandler('kbModalBeforeSubmit', [xhr, settings]); + }, + success: function (data, status, xhr) { + var contentType = xhr.getResponseHeader('content-type') || ''; + if (contentType.indexOf('html') > -1) { + // Assume form contains errors if html + this.injectHtml(data); + } + jQuery(this.element).triggerHandler('kbModalSubmit', [data, status, xhr]); + } + }); + + return false; + }; + + $.fn[pluginName] = function (options) { + return this.each(function () { + if (!$.data(this, pluginName)) { + $.data(this, pluginName, new ModalAjax(this, options)); + } + }); + }; +})(jQuery);