diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..bdb0cab
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,17 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Custom for Visual Studio
+*.cs diff=csharp
+
+# Standard to msysgit
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1b3ae7d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+nbproject
+vendor
+composer.phar
+composer.lock
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+.idea/
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..4697d26
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "hoaaah/yii2-ajaxcrud-bs4",
+ "description": "Gii CRUD template for Single Page Ajax Administration for yii2",
+ "type": "yii2-extension",
+ "keywords": ["yii2","extension","ajax","crud","gii","template","database"],
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "John Martin",
+ "email": "john.itvn@gmail.com",
+ "homepage": "https://github.com/johnitvn?tab=repositories"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/johnitvn/yii2-ajaxcrud/issues?state=open",
+ "source": "https://github.com/johnitvn/yii2-ajaxcrud"
+ },
+ "require": {
+ "yiisoft/yii2": "*",
+ "yiisoft/yii2-gii": "*",
+ "yiisoft/yii2-bootstrap": "*",
+ "kartik-v/yii2-grid": "^3.0.4",
+ "kartik-v/yii2-mpdf": "^1.0.0",
+ "kartik-v/yii2-editable": "^1.7.3"
+ },
+ "autoload": {
+ "psr-4": {
+ "hoaaah\\ajaxcrud\\": "src/"
+ }
+ },
+ "extra": {
+ "bootstrap": "hoaaah\\ajaxcrud\\Bootstrap"
+ }
+}
diff --git a/src/Bootstrap.php b/src/Bootstrap.php
new file mode 100644
index 0000000..217277f
--- /dev/null
+++ b/src/Bootstrap.php
@@ -0,0 +1,30 @@
+
+ * @since 1.0
+ */
+class Bootstrap implements BootstrapInterface {
+
+ /**
+ * Bootstrap method to be called during application bootstrap stage.
+ *
+ * @param Application $app the application currently running
+ */
+ public function bootstrap($app) {
+ Yii::setAlias("@ajaxcrud", __DIR__);
+ Yii::setAlias("@hoaaah/ajaxcrud", __DIR__);
+ if ($app->hasModule('gii')) {
+ if (!isset($app->getModule('gii')->generators['ajaxcrud'])) {
+ $app->getModule('gii')->generators['ajaxcrud'] = 'hoaaah\ajaxcrud\generators\Generator';
+ }
+ }
+ }
+
+}
diff --git a/src/BulkButtonWidget.php b/src/BulkButtonWidget.php
new file mode 100644
index 0000000..d10c2eb
--- /dev/null
+++ b/src/BulkButtonWidget.php
@@ -0,0 +1,24 @@
+'.
+ ' With selected '.
+ $this->buttons.
+ '';
+ return $content;
+ }
+}
+?>
diff --git a/src/CrudAsset.php b/src/CrudAsset.php
new file mode 100644
index 0000000..4abc467
--- /dev/null
+++ b/src/CrudAsset.php
@@ -0,0 +1,42 @@
+
+ * @since 1.0
+ */
+class CrudAsset extends AssetBundle
+{
+ public $sourcePath = '@ajaxcrud/assets';
+
+// public $publishOptions = [
+// 'forceCopy' => true,
+// ];
+
+ public $css = [
+ 'ajaxcrud.css'
+ ];
+
+ public $depends = [
+ 'yii\web\YiiAsset',
+ 'yii\bootstrap\BootstrapAsset',
+ 'yii\bootstrap\BootstrapPluginAsset',
+ 'kartik\grid\GridViewAsset',
+ ];
+
+ public function init() {
+ // In dev mode use non-minified javascripts
+ $this->js = YII_DEBUG ? [
+ 'ModalRemote.js',
+ 'ajaxcrud.js',
+ ]:[
+ 'ModalRemote.min.js',
+ 'ajaxcrud.min.js',
+ ];
+
+ parent::init();
+ }
+}
diff --git a/src/assets/ModalRemote.js b/src/assets/ModalRemote.js
new file mode 100644
index 0000000..0b7b616
--- /dev/null
+++ b/src/assets/ModalRemote.js
@@ -0,0 +1,391 @@
+/*!
+ * Modal Remote
+ * =================================
+ * Use for johnitvn/yii2-ajaxcrud extension
+ * @author John Martin john.itvn@gmail.com
+ */
+(function ($) {
+ $.fn.hasAttr = function (name) {
+ return this.attr(name) !== undefined;
+ };
+}(jQuery));
+
+
+function ModalRemote(modalId) {
+
+ this.defaults = {
+ okLabel: "OK",
+ executeLabel: "Execute",
+ cancelLabel: "Cancel",
+ loadingTitle: "Loading"
+ };
+
+ this.modal = $(modalId);
+
+ this.dialog = $(modalId).find('.modal-dialog');
+
+ this.header = $(modalId).find('.modal-header');
+
+ this.content = $(modalId).find('.modal-body');
+
+ this.footer = $(modalId).find('.modal-footer');
+
+ this.loadingContent = '
';
+
+
+ /**
+ * Show the modal
+ */
+ this.show = function () {
+ this.clear();
+ $(this.modal).modal('show');
+ };
+
+ /**
+ * Hide the modal
+ */
+ this.hide = function () {
+ $(this.modal).modal('hide');
+ };
+
+ /**
+ * Toogle show/hide modal
+ */
+ this.toggle = function () {
+ $(this.modal).modal('toggle');
+ };
+
+ /**
+ * Clear modal
+ */
+ this.clear = function () {
+ $(this.modal).find('.modal-title').remove();
+ $(this.content).html("");
+ $(this.footer).html("");
+ };
+
+ /**
+ * Set size of modal
+ * @param {string} size large/normal/small
+ */
+ this.setSize = function (size) {
+ $(this.dialog).removeClass('modal-lg');
+ $(this.dialog).removeClass('modal-sm');
+ if (size == 'large')
+ $(this.dialog).addClass('modal-lg');
+ else if (size == 'small')
+ $(this.dialog).addClass('modal-sm');
+ else if (size !== 'normal')
+ console.warn("Undefined size " + size);
+ };
+
+ /**
+ * Set modal header
+ * @param {string} content The content of modal header
+ */
+ this.setHeader = function (content) {
+ $(this.header).html(content);
+ };
+
+ /**
+ * Set modal content
+ * @param {string} content The content of modal content
+ */
+ this.setContent = function (content) {
+ $(this.content).html(content);
+ };
+
+ /**
+ * Set modal footer
+ * @param {string} content The content of modal footer
+ */
+ this.setFooter = function (content) {
+ $(this.footer).html(content);
+ };
+
+ /**
+ * Set modal footer
+ * @param {string} title The title of modal
+ */
+ this.setTitle = function (title) {
+ // remove old title
+ $(this.header).find('h4.modal-title').remove();
+ // add new title
+ $(this.header).append('' + title + '
');
+ };
+
+ /**
+ * Hide close button
+ */
+ this.hidenCloseButton = function () {
+ $(this.header).find('button.close').hide();
+ };
+
+ /**
+ * Show close button
+ */
+ this.showCloseButton = function () {
+ $(this.header).find('button.close').show();
+ };
+
+ /**
+ * Show loading state in modal
+ */
+ this.displayLoading = function () {
+ this.setContent(this.loadingContent);
+ this.setTitle(this.defaults.loadingTitle);
+ };
+
+ /**
+ * Add button to footer
+ * @param string label The label of button
+ * @param string classes The class of button
+ * @param callable callback the callback when button click
+ */
+ this.addFooterButton = function (label, type, classes, callback) {
+ buttonElm = document.createElement('button');
+ buttonElm.setAttribute('type', type === null ? 'button' : type);
+ buttonElm.setAttribute('class', classes === null ? 'btn btn-primary' : classes);
+ buttonElm.innerHTML = label;
+ var instance = this;
+ $(this.footer).append(buttonElm);
+ if (callback !== null) {
+ $(buttonElm).click(function (event) {
+ callback.call(instance, this, event);
+ });
+ }
+ };
+
+ /**
+ * Send ajax request and wraper response to modal
+ * @param {string} url The url of request
+ * @param {string} method The method of request
+ * @param {object}data of request
+ */
+ this.doRemote = function (url, method, data) {
+ var instance = this;
+ $.ajax({
+ url: url,
+ method: method,
+ data: data,
+ async: false,
+ beforeSend: function () {
+ beforeRemoteRequest.call(instance);
+ },
+ error: function (response) {
+ errorRemoteResponse.call(instance, response);
+ },
+ success: function (response) {
+ successRemoteResponse.call(instance, response);
+ },
+ contentType: false,
+ cache: false,
+ processData: false
+ });
+ };
+
+ /**
+ * Before send request process
+ * - Ensure clear and show modal
+ * - Show loading state in modal
+ */
+ function beforeRemoteRequest() {
+ this.show();
+ this.displayLoading();
+ }
+
+
+ /**
+ * When remote sends error response
+ * @param {string} response
+ */
+ function errorRemoteResponse(response) {
+ this.setTitle(response.status + response.statusText);
+ this.setContent(response.responseText);
+ this.addFooterButton('Close', 'button', 'btn btn-default', function (button, event) {
+ this.hide();
+ })
+ }
+
+ /**
+ * When remote sends success response
+ * @param {string} response
+ */
+ function successRemoteResponse(response) {
+
+ // Reload datatable if response contain forceReload field
+ if (response.forceReload !== undefined && response.forceReload) {
+ if (response.forceReload == 'true') {
+ // Backwards compatible reload of fixed crud-datatable-pjax
+ $.pjax.reload({container: '#crud-datatable-pjax'});
+ } else {
+ $.pjax.reload({container: response.forceReload});
+ }
+ }
+
+ // Close modal if response contains forceClose field
+ if (response.forceClose !== undefined && response.forceClose) {
+ this.hide();
+ return;
+ }
+
+ if (response.size !== undefined)
+ this.setSize(response.size);
+
+ if (response.title !== undefined)
+ this.setTitle(response.title);
+
+ if (response.content !== undefined)
+ this.setContent(response.content);
+
+ if (response.footer !== undefined)
+ this.setFooter(response.footer);
+
+ if ($(this.content).find("form")[0] !== undefined) {
+ this.setupFormSubmit(
+ $(this.content).find("form")[0],
+ $(this.footer).find('[type="submit"]')[0]
+ );
+ }
+ }
+
+ /**
+ * Prepare submit button when modal has form
+ * @param {string} modalForm
+ * @param {object} modalFormSubmitBtn
+ */
+ this.setupFormSubmit = function (modalForm, modalFormSubmitBtn) {
+
+ if (modalFormSubmitBtn === undefined) {
+ // If submit button not found throw warning message
+ console.warn('Modal has form but does not have a submit button');
+ } else {
+ var instance = this;
+
+ // Submit form when user clicks submit button
+ $(modalFormSubmitBtn).click(function (e) {
+ var data;
+
+ // Test if browser supports FormData which handles uploads
+ if (window.FormData) {
+ data = new FormData($(modalForm)[0]);
+ } else {
+ // Fallback to serialize
+ data = $(modalForm).serializeArray();
+ }
+
+ instance.doRemote(
+ $(modalForm).attr('action'),
+ $(modalForm).hasAttr('method') ? $(modalForm).attr('method') : 'GET',
+ data
+ );
+ });
+ }
+ };
+
+ /**
+ * Show the confirm dialog
+ * @param {string} title The title of modal
+ * @param {string} message The message for ask user
+ * @param {string} okLabel The label of ok button
+ * @param {string} cancelLabel The class of cancel button
+ * @param {string} size The size of the modal
+ * @param {string} dataUrl Where to post
+ * @param {string} dataRequestMethod POST or GET
+ * @param {number[]} selectedIds
+ */
+ this.confirmModal = function (title, message, okLabel, cancelLabel, size, dataUrl, dataRequestMethod, selectedIds) {
+ this.show();
+ this.setSize(size);
+
+ if (title !== undefined) {
+ this.setTitle(title);
+ }
+ // Add form for user input if required
+ this.setContent('