diff --git a/Editable.php b/Editable.php index 4d5833f..2efe430 100644 --- a/Editable.php +++ b/Editable.php @@ -56,6 +56,7 @@ public function init() if ($this->url === null) { throw new InvalidConfigException("You must setup the 'Url' property."); } + if (!isset($this->options['id'])) { $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); } @@ -111,6 +112,7 @@ protected function registerClientScript() /** * Return plugin options in json format + * * @return string */ public function getPluginOptions() @@ -130,6 +132,7 @@ public function getPluginOptions() /** * Register client events + * * @param $id */ public function registerClientEvents($id) @@ -144,6 +147,7 @@ public function registerClientEvents($id) /** * Return link text + * * @return mixed|string */ protected function getLinkText() @@ -169,6 +173,7 @@ protected function getLinkText() /** * To ensure that `getPrimaryKey()` and `getIsNewRecord()` methods are implemented in model. + * * @return bool */ protected function hasActiveRecord() diff --git a/EditableAction.php b/EditableAction.php index 9ad2b32..efb416b 100644 --- a/EditableAction.php +++ b/EditableAction.php @@ -17,19 +17,23 @@ class EditableAction extends Action * @var string the class name to handle */ public $modelClass; + /** * @var string the scenario to be used (optional) */ public $scenario; + /** * @var \Closure a function to be called previous saving model. The anonymous function is preferable to have the * model passed by reference. This is useful when we need to set model with extra data previous update. */ public $preProcess; + /** * @var bool whether to create a model if a primary key parameter was not found. */ public $forceCreate = true; + /** * @var string default pk column name */ @@ -48,6 +52,7 @@ public function init() /** * Runs the action + * * @return bool * @throws BadRequestHttpException */ @@ -62,12 +67,15 @@ public function run() $attribute = array_pop($attributeParts); } $value = Yii::$app->request->post('value'); + if ($attribute === null) { throw new BadRequestHttpException("Attribute cannot be empty."); } + if ($value === null) { throw new BadRequestHttpException("Value cannot be empty."); } + /** @var \Yii\db\ActiveRecord $model */ $model = $class::findOne(is_array($pk) ? $pk : [$this->pkColumn => $pk]); if (!$model) { @@ -77,13 +85,16 @@ public function run() throw new BadRequestHttpException('Entity not found by primary key ' . $pk); } } + // do we have a preProcess function if ($this->preProcess && is_callable($this->preProcess, true)) { call_user_func($this->preProcess, $model); } + if ($this->scenario !== null) { $model->setScenario($this->scenario); } + $model->$attribute = $value; if ($model->validate([$attribute])) { @@ -93,5 +104,4 @@ public function run() throw new BadRequestHttpException($model->getFirstError($attribute)); } } - -} +} \ No newline at end of file diff --git a/EditableColumn.php b/EditableColumn.php index 5f0b53e..c3cd5ee 100644 --- a/EditableColumn.php +++ b/EditableColumn.php @@ -22,14 +22,17 @@ class EditableColumn extends DataColumn * Editable options */ public $editableOptions = []; + /** * @var string suffix substituted to a name class of the tag */ public $classSuffix; + /** * @var string the url to post */ public $url; + /** * @var string the type of editor */ @@ -59,6 +62,7 @@ public function init() /** * Renders the data cell content. + * * @param mixed $model the data model * @param mixed $key the key associated with the data model * @param integer $index the zero-based index of the data model among the models array returned by [[GridView::dataProvider]]. diff --git a/assets/editable/css/bootstrap-editable.css b/assets/editable/css/bootstrap-editable.css deleted file mode 100644 index eaef0de..0000000 --- a/assets/editable/css/bootstrap-editable.css +++ /dev/null @@ -1,663 +0,0 @@ -/*! X-editable - v1.5.1 -* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery -* http://github.com/vitalets/x-editable -* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ -.editableform { - margin-bottom: 0; /* overwrites bootstrap margin */ -} - -.editableform .control-group { - margin-bottom: 0; /* overwrites bootstrap margin */ - white-space: nowrap; /* prevent wrapping buttons on new line */ - line-height: 20px; /* overwriting bootstrap line-height. See #133 */ -} - -/* - BS3 width:1005 for inputs breaks editable form in popup - See: https://github.com/vitalets/x-editable/issues/393 -*/ -.editableform .form-control { - width: auto; -} - -.editable-buttons { - display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */ - vertical-align: top; - margin-left: 7px; - /* inline-block emulation for IE7*/ - zoom: 1; - *display: inline; -} - -.editable-buttons.editable-buttons-bottom { - display: block; - margin-top: 7px; - margin-left: 0; -} - -.editable-input { - vertical-align: top; - display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */ - width: auto; /* bootstrap-responsive has width: 100% that breakes layout */ - white-space: normal; /* reset white-space decalred in parent*/ - /* display-inline emulation for IE7*/ - zoom: 1; - *display: inline; -} - -.editable-buttons .editable-cancel { - margin-left: 7px; -} - -/*for jquery-ui buttons need set height to look more pretty*/ -.editable-buttons button.ui-button-icon-only { - height: 24px; - width: 30px; -} - -.editableform-loading { - background: url('../img/loading.gif') center center no-repeat; - height: 25px; - width: auto; - min-width: 25px; -} - -.editable-inline .editableform-loading { - background-position: left 5px; -} - - .editable-error-block { - max-width: 300px; - margin: 5px 0 0 0; - width: auto; - white-space: normal; -} - -/*add padding for jquery ui*/ -.editable-error-block.ui-state-error { - padding: 3px; -} - -.editable-error { - color: red; -} - -/* ---- For specific types ---- */ - -.editableform .editable-date { - padding: 0; - margin: 0; - float: left; -} - -/* move datepicker icon to center of add-on button. See https://github.com/vitalets/x-editable/issues/183 */ -.editable-inline .add-on .icon-th { - margin-top: 3px; - margin-left: 1px; -} - - -/* checklist vertical alignment */ -.editable-checklist label input[type="checkbox"], -.editable-checklist label span { - vertical-align: middle; - margin: 0; -} - -.editable-checklist label { - white-space: nowrap; -} - -/* set exact width of textarea to fit buttons toolbar */ -.editable-wysihtml5 { - width: 566px; - height: 250px; -} - -/* clear button shown as link in date inputs */ -.editable-clear { - clear: both; - font-size: 0.9em; - text-decoration: none; - text-align: right; -} - -/* IOS-style clear button for text inputs */ -.editable-clear-x { - background: url('../img/clear.png') center center no-repeat; - display: block; - width: 13px; - height: 13px; - position: absolute; - opacity: 0.6; - z-index: 100; - - top: 50%; - right: 6px; - margin-top: -6px; - -} - -.editable-clear-x:hover { - opacity: 1; -} - -.editable-pre-wrapped { - white-space: pre-wrap; -} -.editable-container.editable-popup { - max-width: none !important; /* without this rule poshytip/tooltip does not stretch */ -} - -.editable-container.popover { - width: auto; /* without this rule popover does not stretch */ -} - -.editable-container.editable-inline { - display: inline-block; - vertical-align: middle; - width: auto; - /* inline-block emulation for IE7*/ - zoom: 1; - *display: inline; -} - -.editable-container.ui-widget { - font-size: inherit; /* jqueryui widget font 1.1em too big, overwrite it */ - z-index: 9990; /* should be less than select2 dropdown z-index to close dropdown first when click */ -} -.editable-click, -a.editable-click, -a.editable-click:hover { - text-decoration: none; - border-bottom: dashed 1px #0088cc; -} - -.editable-click.editable-disabled, -a.editable-click.editable-disabled, -a.editable-click.editable-disabled:hover { - color: #585858; - cursor: default; - border-bottom: none; -} - -.editable-empty, .editable-empty:hover, .editable-empty:focus{ - font-style: italic; - color: #DD1144; - /* border-bottom: none; */ - text-decoration: none; -} - -.editable-unsaved { - font-weight: bold; -} - -.editable-unsaved:after { -/* content: '*'*/ -} - -.editable-bg-transition { - -webkit-transition: background-color 1400ms ease-out; - -moz-transition: background-color 1400ms ease-out; - -o-transition: background-color 1400ms ease-out; - -ms-transition: background-color 1400ms ease-out; - transition: background-color 1400ms ease-out; -} - -/*see https://github.com/vitalets/x-editable/issues/139 */ -.form-horizontal .editable -{ - padding-top: 5px; - display:inline-block; -} - - -/*! - * Datepicker for Bootstrap - * - * Copyright 2012 Stefan Petre - * Improvements by Andrew Rowls - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - */ -.datepicker { - padding: 4px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - direction: ltr; - /*.dow { - border-top: 1px solid #ddd !important; - }*/ - -} -.datepicker-inline { - width: 220px; -} -.datepicker.datepicker-rtl { - direction: rtl; -} -.datepicker.datepicker-rtl table tr td span { - float: right; -} -.datepicker-dropdown { - top: 0; - left: 0; -} -.datepicker-dropdown:before { - content: ''; - display: inline-block; - border-left: 7px solid transparent; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-bottom-color: rgba(0, 0, 0, 0.2); - position: absolute; - top: -7px; - left: 6px; -} -.datepicker-dropdown:after { - content: ''; - display: inline-block; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid #ffffff; - position: absolute; - top: -6px; - left: 7px; -} -.datepicker > div { - display: none; -} -.datepicker.days div.datepicker-days { - display: block; -} -.datepicker.months div.datepicker-months { - display: block; -} -.datepicker.years div.datepicker-years { - display: block; -} -.datepicker table { - margin: 0; -} -.datepicker td, -.datepicker th { - text-align: center; - width: 20px; - height: 20px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - border: none; -} -.table-striped .datepicker table tr td, -.table-striped .datepicker table tr th { - background-color: transparent; -} -.datepicker table tr td.day:hover { - background: #eeeeee; - cursor: pointer; -} -.datepicker table tr td.old, -.datepicker table tr td.new { - color: #999999; -} -.datepicker table tr td.disabled, -.datepicker table tr td.disabled:hover { - background: none; - color: #999999; - cursor: default; -} -.datepicker table tr td.today, -.datepicker table tr td.today:hover, -.datepicker table tr td.today.disabled, -.datepicker table tr td.today.disabled:hover { - background-color: #fde19a; - background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a); - background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a)); - background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a); - background-image: -o-linear-gradient(top, #fdd49a, #fdf59a); - background-image: linear-gradient(top, #fdd49a, #fdf59a); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0); - border-color: #fdf59a #fdf59a #fbed50; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - color: #000; -} -.datepicker table tr td.today:hover, -.datepicker table tr td.today:hover:hover, -.datepicker table tr td.today.disabled:hover, -.datepicker table tr td.today.disabled:hover:hover, -.datepicker table tr td.today:active, -.datepicker table tr td.today:hover:active, -.datepicker table tr td.today.disabled:active, -.datepicker table tr td.today.disabled:hover:active, -.datepicker table tr td.today.active, -.datepicker table tr td.today:hover.active, -.datepicker table tr td.today.disabled.active, -.datepicker table tr td.today.disabled:hover.active, -.datepicker table tr td.today.disabled, -.datepicker table tr td.today:hover.disabled, -.datepicker table tr td.today.disabled.disabled, -.datepicker table tr td.today.disabled:hover.disabled, -.datepicker table tr td.today[disabled], -.datepicker table tr td.today:hover[disabled], -.datepicker table tr td.today.disabled[disabled], -.datepicker table tr td.today.disabled:hover[disabled] { - background-color: #fdf59a; -} -.datepicker table tr td.today:active, -.datepicker table tr td.today:hover:active, -.datepicker table tr td.today.disabled:active, -.datepicker table tr td.today.disabled:hover:active, -.datepicker table tr td.today.active, -.datepicker table tr td.today:hover.active, -.datepicker table tr td.today.disabled.active, -.datepicker table tr td.today.disabled:hover.active { - background-color: #fbf069 \9; -} -.datepicker table tr td.today:hover:hover { - color: #000; -} -.datepicker table tr td.today.active:hover { - color: #fff; -} -.datepicker table tr td.range, -.datepicker table tr td.range:hover, -.datepicker table tr td.range.disabled, -.datepicker table tr td.range.disabled:hover { - background: #eeeeee; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} -.datepicker table tr td.range.today, -.datepicker table tr td.range.today:hover, -.datepicker table tr td.range.today.disabled, -.datepicker table tr td.range.today.disabled:hover { - background-color: #f3d17a; - background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a); - background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a)); - background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a); - background-image: -o-linear-gradient(top, #f3c17a, #f3e97a); - background-image: linear-gradient(top, #f3c17a, #f3e97a); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0); - border-color: #f3e97a #f3e97a #edde34; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} -.datepicker table tr td.range.today:hover, -.datepicker table tr td.range.today:hover:hover, -.datepicker table tr td.range.today.disabled:hover, -.datepicker table tr td.range.today.disabled:hover:hover, -.datepicker table tr td.range.today:active, -.datepicker table tr td.range.today:hover:active, -.datepicker table tr td.range.today.disabled:active, -.datepicker table tr td.range.today.disabled:hover:active, -.datepicker table tr td.range.today.active, -.datepicker table tr td.range.today:hover.active, -.datepicker table tr td.range.today.disabled.active, -.datepicker table tr td.range.today.disabled:hover.active, -.datepicker table tr td.range.today.disabled, -.datepicker table tr td.range.today:hover.disabled, -.datepicker table tr td.range.today.disabled.disabled, -.datepicker table tr td.range.today.disabled:hover.disabled, -.datepicker table tr td.range.today[disabled], -.datepicker table tr td.range.today:hover[disabled], -.datepicker table tr td.range.today.disabled[disabled], -.datepicker table tr td.range.today.disabled:hover[disabled] { - background-color: #f3e97a; -} -.datepicker table tr td.range.today:active, -.datepicker table tr td.range.today:hover:active, -.datepicker table tr td.range.today.disabled:active, -.datepicker table tr td.range.today.disabled:hover:active, -.datepicker table tr td.range.today.active, -.datepicker table tr td.range.today:hover.active, -.datepicker table tr td.range.today.disabled.active, -.datepicker table tr td.range.today.disabled:hover.active { - background-color: #efe24b \9; -} -.datepicker table tr td.selected, -.datepicker table tr td.selected:hover, -.datepicker table tr td.selected.disabled, -.datepicker table tr td.selected.disabled:hover { - background-color: #9e9e9e; - background-image: -moz-linear-gradient(top, #b3b3b3, #808080); - background-image: -ms-linear-gradient(top, #b3b3b3, #808080); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080)); - background-image: -webkit-linear-gradient(top, #b3b3b3, #808080); - background-image: -o-linear-gradient(top, #b3b3b3, #808080); - background-image: linear-gradient(top, #b3b3b3, #808080); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0); - border-color: #808080 #808080 #595959; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - color: #fff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} -.datepicker table tr td.selected:hover, -.datepicker table tr td.selected:hover:hover, -.datepicker table tr td.selected.disabled:hover, -.datepicker table tr td.selected.disabled:hover:hover, -.datepicker table tr td.selected:active, -.datepicker table tr td.selected:hover:active, -.datepicker table tr td.selected.disabled:active, -.datepicker table tr td.selected.disabled:hover:active, -.datepicker table tr td.selected.active, -.datepicker table tr td.selected:hover.active, -.datepicker table tr td.selected.disabled.active, -.datepicker table tr td.selected.disabled:hover.active, -.datepicker table tr td.selected.disabled, -.datepicker table tr td.selected:hover.disabled, -.datepicker table tr td.selected.disabled.disabled, -.datepicker table tr td.selected.disabled:hover.disabled, -.datepicker table tr td.selected[disabled], -.datepicker table tr td.selected:hover[disabled], -.datepicker table tr td.selected.disabled[disabled], -.datepicker table tr td.selected.disabled:hover[disabled] { - background-color: #808080; -} -.datepicker table tr td.selected:active, -.datepicker table tr td.selected:hover:active, -.datepicker table tr td.selected.disabled:active, -.datepicker table tr td.selected.disabled:hover:active, -.datepicker table tr td.selected.active, -.datepicker table tr td.selected:hover.active, -.datepicker table tr td.selected.disabled.active, -.datepicker table tr td.selected.disabled:hover.active { - background-color: #666666 \9; -} -.datepicker table tr td.active, -.datepicker table tr td.active:hover, -.datepicker table tr td.active.disabled, -.datepicker table tr td.active.disabled:hover { - background-color: #006dcc; - background-image: -moz-linear-gradient(top, #0088cc, #0044cc); - background-image: -ms-linear-gradient(top, #0088cc, #0044cc); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); - background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); - background-image: -o-linear-gradient(top, #0088cc, #0044cc); - background-image: linear-gradient(top, #0088cc, #0044cc); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); - border-color: #0044cc #0044cc #002a80; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - color: #fff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} -.datepicker table tr td.active:hover, -.datepicker table tr td.active:hover:hover, -.datepicker table tr td.active.disabled:hover, -.datepicker table tr td.active.disabled:hover:hover, -.datepicker table tr td.active:active, -.datepicker table tr td.active:hover:active, -.datepicker table tr td.active.disabled:active, -.datepicker table tr td.active.disabled:hover:active, -.datepicker table tr td.active.active, -.datepicker table tr td.active:hover.active, -.datepicker table tr td.active.disabled.active, -.datepicker table tr td.active.disabled:hover.active, -.datepicker table tr td.active.disabled, -.datepicker table tr td.active:hover.disabled, -.datepicker table tr td.active.disabled.disabled, -.datepicker table tr td.active.disabled:hover.disabled, -.datepicker table tr td.active[disabled], -.datepicker table tr td.active:hover[disabled], -.datepicker table tr td.active.disabled[disabled], -.datepicker table tr td.active.disabled:hover[disabled] { - background-color: #0044cc; -} -.datepicker table tr td.active:active, -.datepicker table tr td.active:hover:active, -.datepicker table tr td.active.disabled:active, -.datepicker table tr td.active.disabled:hover:active, -.datepicker table tr td.active.active, -.datepicker table tr td.active:hover.active, -.datepicker table tr td.active.disabled.active, -.datepicker table tr td.active.disabled:hover.active { - background-color: #003399 \9; -} -.datepicker table tr td span { - display: block; - width: 23%; - height: 54px; - line-height: 54px; - float: left; - margin: 1%; - cursor: pointer; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.datepicker table tr td span:hover { - background: #eeeeee; -} -.datepicker table tr td span.disabled, -.datepicker table tr td span.disabled:hover { - background: none; - color: #999999; - cursor: default; -} -.datepicker table tr td span.active, -.datepicker table tr td span.active:hover, -.datepicker table tr td span.active.disabled, -.datepicker table tr td span.active.disabled:hover { - background-color: #006dcc; - background-image: -moz-linear-gradient(top, #0088cc, #0044cc); - background-image: -ms-linear-gradient(top, #0088cc, #0044cc); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); - background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); - background-image: -o-linear-gradient(top, #0088cc, #0044cc); - background-image: linear-gradient(top, #0088cc, #0044cc); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); - border-color: #0044cc #0044cc #002a80; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); - color: #fff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} -.datepicker table tr td span.active:hover, -.datepicker table tr td span.active:hover:hover, -.datepicker table tr td span.active.disabled:hover, -.datepicker table tr td span.active.disabled:hover:hover, -.datepicker table tr td span.active:active, -.datepicker table tr td span.active:hover:active, -.datepicker table tr td span.active.disabled:active, -.datepicker table tr td span.active.disabled:hover:active, -.datepicker table tr td span.active.active, -.datepicker table tr td span.active:hover.active, -.datepicker table tr td span.active.disabled.active, -.datepicker table tr td span.active.disabled:hover.active, -.datepicker table tr td span.active.disabled, -.datepicker table tr td span.active:hover.disabled, -.datepicker table tr td span.active.disabled.disabled, -.datepicker table tr td span.active.disabled:hover.disabled, -.datepicker table tr td span.active[disabled], -.datepicker table tr td span.active:hover[disabled], -.datepicker table tr td span.active.disabled[disabled], -.datepicker table tr td span.active.disabled:hover[disabled] { - background-color: #0044cc; -} -.datepicker table tr td span.active:active, -.datepicker table tr td span.active:hover:active, -.datepicker table tr td span.active.disabled:active, -.datepicker table tr td span.active.disabled:hover:active, -.datepicker table tr td span.active.active, -.datepicker table tr td span.active:hover.active, -.datepicker table tr td span.active.disabled.active, -.datepicker table tr td span.active.disabled:hover.active { - background-color: #003399 \9; -} -.datepicker table tr td span.old, -.datepicker table tr td span.new { - color: #999999; -} -.datepicker th.datepicker-switch { - width: 145px; -} -.datepicker thead tr:first-child th, -.datepicker tfoot tr th { - cursor: pointer; -} -.datepicker thead tr:first-child th:hover, -.datepicker tfoot tr th:hover { - background: #eeeeee; -} -.datepicker .cw { - font-size: 10px; - width: 12px; - padding: 0 2px 0 5px; - vertical-align: middle; -} -.datepicker thead tr:first-child th.cw { - cursor: default; - background-color: transparent; -} -.input-append.date .add-on i, -.input-prepend.date .add-on i { - display: block; - cursor: pointer; - width: 16px; - height: 16px; -} -.input-daterange input { - text-align: center; -} -.input-daterange input:first-child { - -webkit-border-radius: 3px 0 0 3px; - -moz-border-radius: 3px 0 0 3px; - border-radius: 3px 0 0 3px; -} -.input-daterange input:last-child { - -webkit-border-radius: 0 3px 3px 0; - -moz-border-radius: 0 3px 3px 0; - border-radius: 0 3px 3px 0; -} -.input-daterange .add-on { - display: inline-block; - width: auto; - min-width: 16px; - height: 18px; - padding: 4px 5px; - font-weight: normal; - line-height: 18px; - text-align: center; - text-shadow: 0 1px 0 #ffffff; - vertical-align: middle; - background-color: #eeeeee; - border: 1px solid #ccc; - margin-left: -5px; - margin-right: -5px; -} diff --git a/assets/editable/img/clear.png b/assets/editable/img/clear.png deleted file mode 100644 index 580b52a..0000000 Binary files a/assets/editable/img/clear.png and /dev/null differ diff --git a/assets/editable/img/loading.gif b/assets/editable/img/loading.gif deleted file mode 100644 index 5b33f7e..0000000 Binary files a/assets/editable/img/loading.gif and /dev/null differ diff --git a/assets/editable/js/bootstrap-editable.js b/assets/editable/js/bootstrap-editable.js deleted file mode 100644 index dd81385..0000000 --- a/assets/editable/js/bootstrap-editable.js +++ /dev/null @@ -1,6807 +0,0 @@ -/*! X-editable - v1.5.1 -* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery -* http://github.com/vitalets/x-editable -* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ -/** -Form with single input element, two buttons and two states: normal/loading. -Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown. -Editableform is linked with one of input types, e.g. 'text', 'select' etc. - -@class editableform -@uses text -@uses textarea -**/ -(function ($) { - "use strict"; - - var EditableForm = function (div, options) { - this.options = $.extend({}, $.fn.editableform.defaults, options); - this.$div = $(div); //div, containing form. Not form tag. Not editable-element. - if(!this.options.scope) { - this.options.scope = this; - } - //nothing shown after init - }; - - EditableForm.prototype = { - constructor: EditableForm, - initInput: function() { //called once - //take input from options (as it is created in editable-element) - this.input = this.options.input; - - //set initial value - //todo: may be add check: typeof str === 'string' ? - this.value = this.input.str2value(this.options.value); - - //prerender: get input.$input - this.input.prerender(); - }, - initTemplate: function() { - this.$form = $($.fn.editableform.template); - }, - initButtons: function() { - var $btn = this.$form.find('.editable-buttons'); - $btn.append($.fn.editableform.buttons); - if(this.options.showbuttons === 'bottom') { - $btn.addClass('editable-buttons-bottom'); - } - }, - /** - Renders editableform - - @method render - **/ - render: function() { - //init loader - this.$loading = $($.fn.editableform.loading); - this.$div.empty().append(this.$loading); - - //init form template and buttons - this.initTemplate(); - if(this.options.showbuttons) { - this.initButtons(); - } else { - this.$form.find('.editable-buttons').remove(); - } - - //show loading state - this.showLoading(); - - //flag showing is form now saving value to server. - //It is needed to wait when closing form. - this.isSaving = false; - - /** - Fired when rendering starts - @event rendering - @param {Object} event event object - **/ - this.$div.triggerHandler('rendering'); - - //init input - this.initInput(); - - //append input to form - this.$form.find('div.editable-input').append(this.input.$tpl); - - //append form to container - this.$div.append(this.$form); - - //render input - $.when(this.input.render()) - .then($.proxy(function () { - //setup input to submit automatically when no buttons shown - if(!this.options.showbuttons) { - this.input.autosubmit(); - } - - //attach 'cancel' handler - this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); - - if(this.input.error) { - this.error(this.input.error); - this.$form.find('.editable-submit').attr('disabled', true); - this.input.$input.attr('disabled', true); - //prevent form from submitting - this.$form.submit(function(e){ e.preventDefault(); }); - } else { - this.error(false); - this.input.$input.removeAttr('disabled'); - this.$form.find('.editable-submit').removeAttr('disabled'); - var value = (this.value === null || this.value === undefined || this.value === '') ? this.options.defaultValue : this.value; - this.input.value2input(value); - //attach submit handler - this.$form.submit($.proxy(this.submit, this)); - } - - /** - Fired when form is rendered - @event rendered - @param {Object} event event object - **/ - this.$div.triggerHandler('rendered'); - - this.showForm(); - - //call postrender method to perform actions required visibility of form - if(this.input.postrender) { - this.input.postrender(); - } - }, this)); - }, - cancel: function() { - /** - Fired when form was cancelled by user - @event cancel - @param {Object} event event object - **/ - this.$div.triggerHandler('cancel'); - }, - showLoading: function() { - var w, h; - if(this.$form) { - //set loading size equal to form - w = this.$form.outerWidth(); - h = this.$form.outerHeight(); - if(w) { - this.$loading.width(w); - } - if(h) { - this.$loading.height(h); - } - this.$form.hide(); - } else { - //stretch loading to fill container width - w = this.$loading.parent().width(); - if(w) { - this.$loading.width(w); - } - } - this.$loading.show(); - }, - - showForm: function(activate) { - this.$loading.hide(); - this.$form.show(); - if(activate !== false) { - this.input.activate(); - } - /** - Fired when form is shown - @event show - @param {Object} event event object - **/ - this.$div.triggerHandler('show'); - }, - - error: function(msg) { - var $group = this.$form.find('.control-group'), - $block = this.$form.find('.editable-error-block'), - lines; - - if(msg === false) { - $group.removeClass($.fn.editableform.errorGroupClass); - $block.removeClass($.fn.editableform.errorBlockClass).empty().hide(); - } else { - //convert newline to
for more pretty error display - if(msg) { - lines = (''+msg).split('\n'); - for (var i = 0; i < lines.length; i++) { - lines[i] = $('
').text(lines[i]).html(); - } - msg = lines.join('
'); - } - $group.addClass($.fn.editableform.errorGroupClass); - $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); - } - }, - - submit: function(e) { - e.stopPropagation(); - e.preventDefault(); - - //get new value from input - var newValue = this.input.input2value(); - - //validation: if validate returns string or truthy value - means error - //if returns object like {newValue: '...'} => submitted value is reassigned to it - var error = this.validate(newValue); - if ($.type(error) === 'object' && error.newValue !== undefined) { - newValue = error.newValue; - this.input.value2input(newValue); - if(typeof error.msg === 'string') { - this.error(error.msg); - this.showForm(); - return; - } - } else if (error) { - this.error(error); - this.showForm(); - return; - } - - //if value not changed --> trigger 'nochange' event and return - /*jslint eqeq: true*/ - if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) { - /*jslint eqeq: false*/ - /** - Fired when value not changed but form is submitted. Requires savenochange = false. - @event nochange - @param {Object} event event object - **/ - this.$div.triggerHandler('nochange'); - return; - } - - //convert value for submitting to server - var submitValue = this.input.value2submit(newValue); - - this.isSaving = true; - - //sending data to server - $.when(this.save(submitValue)) - .done($.proxy(function(response) { - this.isSaving = false; - - //run success callback - var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; - - //if success callback returns false --> keep form open and do not activate input - if(res === false) { - this.error(false); - this.showForm(false); - return; - } - - //if success callback returns string --> keep form open, show error and activate input - if(typeof res === 'string') { - this.error(res); - this.showForm(); - return; - } - - //if success callback returns object like {newValue: } --> use that value instead of submitted - //it is usefull if you want to chnage value in url-function - if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) { - newValue = res.newValue; - } - - //clear error message - this.error(false); - this.value = newValue; - /** - Fired when form is submitted - @event save - @param {Object} event event object - @param {Object} params additional params - @param {mixed} params.newValue raw new value - @param {mixed} params.submitValue submitted value as string - @param {Object} params.response ajax response - - @example - $('#form-div').on('save'), function(e, params){ - if(params.newValue === 'username') {...} - }); - **/ - this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); - }, this)) - .fail($.proxy(function(xhr) { - this.isSaving = false; - - var msg; - if(typeof this.options.error === 'function') { - msg = this.options.error.call(this.options.scope, xhr, newValue); - } else { - msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; - } - - this.error(msg); - this.showForm(); - }, this)); - }, - - save: function(submitValue) { - //try parse composite pk defined as json string in data-pk - this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); - - var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, - /* - send on server in following cases: - 1. url is function - 2. url is string AND (pk defined OR send option = always) - */ - send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), - params; - - if (send) { //send to server - this.showLoading(); - - //standard params - params = { - name: this.options.name || '', - value: submitValue, - pk: pk - }; - - //additional params - if(typeof this.options.params === 'function') { - params = this.options.params.call(this.options.scope, params); - } else { - //try parse json in single quotes (from data-params attribute) - this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); - $.extend(params, this.options.params); - } - - if(typeof this.options.url === 'function') { //user's function - return this.options.url.call(this.options.scope, params); - } else { - //send ajax to server and return deferred object - return $.ajax($.extend({ - url : this.options.url, - data : params, - type : 'POST' - }, this.options.ajaxOptions)); - } - } - }, - - validate: function (value) { - if (value === undefined) { - value = this.value; - } - if (typeof this.options.validate === 'function') { - return this.options.validate.call(this.options.scope, value); - } - }, - - option: function(key, value) { - if(key in this.options) { - this.options[key] = value; - } - - if(key === 'value') { - this.setValue(value); - } - - //do not pass option to input as it is passed in editable-element - }, - - setValue: function(value, convertStr) { - if(convertStr) { - this.value = this.input.str2value(value); - } else { - this.value = value; - } - - //if form is visible, update input - if(this.$form && this.$form.is(':visible')) { - this.input.value2input(this.value); - } - } - }; - - /* - Initialize editableform. Applied to jQuery object. - - @method $().editableform(options) - @params {Object} options - @example - var $form = $('<div>').editableform({ - type: 'text', - name: 'username', - url: '/post', - value: 'vitaliy' - }); - - //to display form you should call 'render' method - $form.editableform('render'); - */ - $.fn.editableform = function (option) { - var args = arguments; - return this.each(function () { - var $this = $(this), - data = $this.data('editableform'), - options = typeof option === 'object' && option; - if (!data) { - $this.data('editableform', (data = new EditableForm(this, options))); - } - - if (typeof option === 'string') { //call method - data[option].apply(data, Array.prototype.slice.call(args, 1)); - } - }); - }; - - //keep link to constructor to allow inheritance - $.fn.editableform.Constructor = EditableForm; - - //defaults - $.fn.editableform.defaults = { - /* see also defaults for input */ - - /** - Type of input. Can be text|textarea|select|date|checklist - - @property type - @type string - @default 'text' - **/ - type: 'text', - /** - Url for submit, e.g. '/post' - If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks. - - @property url - @type string|function - @default null - @example - url: function(params) { - var d = new $.Deferred; - if(params.value === 'abc') { - return d.reject('error message'); //returning error via deferred object - } else { - //async saving data in js model - someModel.asyncSaveMethod({ - ..., - success: function(){ - d.resolve(); - } - }); - return d.promise(); - } - } - **/ - url:null, - /** - Additional params for submit. If defined as object - it is **appended** to original ajax data (pk, name and value). - If defined as function - returned object **overwrites** original ajax data. - @example - params: function(params) { - //originally params contain pk, name and value - params.a = 1; - return params; - } - - @property params - @type object|function - @default null - **/ - params:null, - /** - Name of field. Will be submitted on server. Can be taken from id attribute - - @property name - @type string - @default null - **/ - name: null, - /** - Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. {id: 1, lang: 'en'}. - Can be calculated dynamically via function. - - @property pk - @type string|object|function - @default null - **/ - pk: null, - /** - Initial value. If not defined - will be taken from element's content. - For __select__ type should be defined (as it is ID of shown text). - - @property value - @type string|object - @default null - **/ - value: null, - /** - Value that will be displayed in input if original field value is empty (`null|undefined|''`). - - @property defaultValue - @type string|object - @default null - @since 1.4.6 - **/ - defaultValue: null, - /** - Strategy for sending data on server. Can be `auto|always|never`. - When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally. - - @property send - @type string - @default 'auto' - **/ - send: 'auto', - /** - Function for client-side validation. If returns string - means validation not passed and string showed as error. - Since 1.5.1 you can modify submitted value by returning object from `validate`: - `{newValue: '...'}` or `{newValue: '...', msg: '...'}` - - @property validate - @type function - @default null - @example - validate: function(value) { - if($.trim(value) == '') { - return 'This field is required'; - } - } - **/ - validate: null, - /** - Success callback. Called when value successfully sent on server and **response status = 200**. - Usefull to work with json response. For example, if your backend response can be {success: true} - or {success: false, msg: "server error"} you can check it inside this callback. - If it returns **string** - means error occured and string is shown as error message. - If it returns **object like** {newValue: <something>} - it overwrites value, submitted by user. - Otherwise newValue simply rendered into element. - - @property success - @type function - @default null - @example - success: function(response, newValue) { - if(!response.success) return response.msg; - } - **/ - success: null, - /** - Error callback. Called when request failed (response status != 200). - Usefull when you want to parse error response and display a custom message. - Must return **string** - the message to be displayed in the error block. - - @property error - @type function - @default null - @since 1.4.4 - @example - error: function(response, newValue) { - if(response.status === 500) { - return 'Service unavailable. Please try later.'; - } else { - return response.responseText; - } - } - **/ - error: null, - /** - Additional options for submit ajax request. - List of values: http://api.jquery.com/jQuery.ajax - - @property ajaxOptions - @type object - @default null - @since 1.1.1 - @example - ajaxOptions: { - type: 'put', - dataType: 'json' - } - **/ - ajaxOptions: null, - /** - Where to show buttons: left(true)|bottom|false - Form without buttons is auto-submitted. - - @property showbuttons - @type boolean|string - @default true - @since 1.1.1 - **/ - showbuttons: true, - /** - Scope for callback methods (success, validate). - If null means editableform instance itself. - - @property scope - @type DOMElement|object - @default null - @since 1.2.0 - @private - **/ - scope: null, - /** - Whether to save or cancel value when it was not changed but form was submitted - - @property savenochange - @type boolean - @default false - @since 1.2.0 - **/ - savenochange: false - }; - - /* - Note: following params could redefined in engine: bootstrap or jqueryui: - Classes 'control-group' and 'editable-error-block' must always present! - */ - $.fn.editableform.template = '
'+ - '
' + - '
'+ - '
' + - '
' + - '
'; - - //loading div - $.fn.editableform.loading = '
'; - - //buttons - $.fn.editableform.buttons = ''+ - ''; - - //error class attached to control-group - $.fn.editableform.errorGroupClass = null; - - //error class attached to editable-error-block - $.fn.editableform.errorBlockClass = 'editable-error'; - - //engine - $.fn.editableform.engine = 'jquery'; -}(window.jQuery)); - -/** -* EditableForm utilites -*/ -(function ($) { - "use strict"; - - //utils - $.fn.editableutils = { - /** - * classic JS inheritance function - */ - inherit: function (Child, Parent) { - var F = function() { }; - F.prototype = Parent.prototype; - Child.prototype = new F(); - Child.prototype.constructor = Child; - Child.superclass = Parent.prototype; - }, - - /** - * set caret position in input - * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area - */ - setCursorPosition: function(elem, pos) { - if (elem.setSelectionRange) { - elem.setSelectionRange(pos, pos); - } else if (elem.createTextRange) { - var range = elem.createTextRange(); - range.collapse(true); - range.moveEnd('character', pos); - range.moveStart('character', pos); - range.select(); - } - }, - - /** - * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes) - * That allows such code as:
- * safe = true --> means no exception will be thrown - * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery - */ - tryParseJson: function(s, safe) { - if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) { - if (safe) { - try { - /*jslint evil: true*/ - s = (new Function('return ' + s))(); - /*jslint evil: false*/ - } catch (e) {} finally { - return s; - } - } else { - /*jslint evil: true*/ - s = (new Function('return ' + s))(); - /*jslint evil: false*/ - } - } - return s; - }, - - /** - * slice object by specified keys - */ - sliceObj: function(obj, keys, caseSensitive /* default: false */) { - var key, keyLower, newObj = {}; - - if (!$.isArray(keys) || !keys.length) { - return newObj; - } - - for (var i = 0; i < keys.length; i++) { - key = keys[i]; - if (obj.hasOwnProperty(key)) { - newObj[key] = obj[key]; - } - - if(caseSensitive === true) { - continue; - } - - //when getting data-* attributes via $.data() it's converted to lowercase. - //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery - //workaround is code below. - keyLower = key.toLowerCase(); - if (obj.hasOwnProperty(keyLower)) { - newObj[key] = obj[keyLower]; - } - } - - return newObj; - }, - - /* - exclude complex objects from $.data() before pass to config - */ - getConfigData: function($element) { - var data = {}; - $.each($element.data(), function(k, v) { - if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) { - data[k] = v; - } - }); - return data; - }, - - /* - returns keys of object - */ - objectKeys: function(o) { - if (Object.keys) { - return Object.keys(o); - } else { - if (o !== Object(o)) { - throw new TypeError('Object.keys called on a non-object'); - } - var k=[], p; - for (p in o) { - if (Object.prototype.hasOwnProperty.call(o,p)) { - k.push(p); - } - } - return k; - } - - }, - - /** - method to escape html. - **/ - escape: function(str) { - return $('
').text(str).html(); - }, - - /* - returns array items from sourceData having value property equal or inArray of 'value' - */ - itemsByValue: function(value, sourceData, valueProp) { - if(!sourceData || value === null) { - return []; - } - - if (typeof(valueProp) !== "function") { - var idKey = valueProp || 'value'; - valueProp = function (e) { return e[idKey]; }; - } - - var isValArray = $.isArray(value), - result = [], - that = this; - - $.each(sourceData, function(i, o) { - if(o.children) { - result = result.concat(that.itemsByValue(value, o.children, valueProp)); - } else { - /*jslint eqeq: true*/ - if(isValArray) { - if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) { - result.push(o); - } - } else { - var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o; - if(value == itemValue) { - result.push(o); - } - } - /*jslint eqeq: false*/ - } - }); - - return result; - }, - - /* - Returns input by options: type, mode. - */ - createInput: function(options) { - var TypeConstructor, typeOptions, input, - type = options.type; - - //`date` is some kind of virtual type that is transformed to one of exact types - //depending on mode and core lib - if(type === 'date') { - //inline - if(options.mode === 'inline') { - if($.fn.editabletypes.datefield) { - type = 'datefield'; - } else if($.fn.editabletypes.dateuifield) { - type = 'dateuifield'; - } - //popup - } else { - if($.fn.editabletypes.date) { - type = 'date'; - } else if($.fn.editabletypes.dateui) { - type = 'dateui'; - } - } - - //if type still `date` and not exist in types, replace with `combodate` that is base input - if(type === 'date' && !$.fn.editabletypes.date) { - type = 'combodate'; - } - } - - //`datetime` should be datetimefield in 'inline' mode - if(type === 'datetime' && options.mode === 'inline') { - type = 'datetimefield'; - } - - //change wysihtml5 to textarea for jquery UI and plain versions - if(type === 'wysihtml5' && !$.fn.editabletypes[type]) { - type = 'textarea'; - } - - //create input of specified type. Input will be used for converting value, not in form - if(typeof $.fn.editabletypes[type] === 'function') { - TypeConstructor = $.fn.editabletypes[type]; - typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults)); - input = new TypeConstructor(typeOptions); - return input; - } else { - $.error('Unknown type: '+ type); - return false; - } - }, - - //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr - supportsTransitions: function () { - var b = document.body || document.documentElement, - s = b.style, - p = 'transition', - v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; - - if(typeof s[p] === 'string') { - return true; - } - - // Tests for vendor specific prop - p = p.charAt(0).toUpperCase() + p.substr(1); - for(var i=0; i -This method applied internally in $().editable(). You should subscribe on it's events (save / cancel) to get profit of it.
-Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.
-Applied as jQuery method. - -@class editableContainer -@uses editableform -**/ -(function ($) { - "use strict"; - - var Popup = function (element, options) { - this.init(element, options); - }; - - var Inline = function (element, options) { - this.init(element, options); - }; - - //methods - Popup.prototype = { - containerName: null, //method to call container on element - containerDataName: null, //object name in element's .data() - innerCss: null, //tbd in child class - containerClass: 'editable-container editable-popup', //css class applied to container element - defaults: {}, //container itself defaults - - init: function(element, options) { - this.$element = $(element); - //since 1.4.1 container do not use data-* directly as they already merged into options. - this.options = $.extend({}, $.fn.editableContainer.defaults, options); - this.splitOptions(); - - //set scope of form callbacks to element - this.formOptions.scope = this.$element[0]; - - this.initContainer(); - - //flag to hide container, when saving value will finish - this.delayedHide = false; - - //bind 'destroyed' listener to destroy container when element is removed from dom - this.$element.on('destroyed', $.proxy(function(){ - this.destroy(); - }, this)); - - //attach document handler to close containers on click / escape - if(!$(document).data('editable-handlers-attached')) { - //close all on escape - $(document).on('keyup.editable', function (e) { - if (e.which === 27) { - $('.editable-open').editableContainer('hide'); - //todo: return focus on element - } - }); - - //close containers when click outside - //(mousedown could be better than click, it closes everything also on drag drop) - $(document).on('click.editable', function(e) { - var $target = $(e.target), i, - exclude_classes = ['.editable-container', - '.ui-datepicker-header', - '.datepicker', //in inline mode datepicker is rendered into body - '.modal-backdrop', - '.bootstrap-wysihtml5-insert-image-modal', - '.bootstrap-wysihtml5-insert-link-modal' - ]; - - //check if element is detached. It occurs when clicking in bootstrap datepicker - if (!$.contains(document.documentElement, e.target)) { - return; - } - - //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document - //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 - //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec - if($target.is(document)) { - return; - } - - //if click inside one of exclude classes --> no nothing - for(i=0; i container changes size before hide. - */ - - //if form already exist - delete previous data - if(this.$form) { - //todo: destroy prev data! - //this.$form.destroy(); - } - - this.$form = $('
'); - - //insert form into container body - if(this.tip().is(this.innerCss)) { - //for inline container - this.tip().append(this.$form); - } else { - this.tip().find(this.innerCss).append(this.$form); - } - - //render form - this.renderForm(); - }, - - /** - Hides container with form - @method hide() - @param {string} reason Reason caused hiding. Can be save|cancel|onblur|nochange|undefined (=manual) - **/ - hide: function(reason) { - if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { - return; - } - - //if form is saving value, schedule hide - if(this.$form.data('editableform').isSaving) { - this.delayedHide = {reason: reason}; - return; - } else { - this.delayedHide = false; - } - - this.$element.removeClass('editable-open'); - this.innerHide(); - - /** - Fired when container was hidden. It occurs on both save or cancel. - **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. - The workaround is to check `arguments.length` that is always `2` for x-editable. - - @event hidden - @param {object} event event object - @param {string} reason Reason caused hiding. Can be save|cancel|onblur|nochange|manual - @example - $('#username').on('hidden', function(e, reason) { - if(reason === 'save' || reason === 'cancel') { - //auto-open next editable - $(this).closest('tr').next().find('.editable').editable('show'); - } - }); - **/ - this.$element.triggerHandler('hidden', reason || 'manual'); - }, - - /* internal show method. To be overwritten in child classes */ - innerShow: function () { - - }, - - /* internal hide method. To be overwritten in child classes */ - innerHide: function () { - - }, - - /** - Toggles container visibility (show / hide) - @method toggle() - @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. - **/ - toggle: function(closeAll) { - if(this.container() && this.tip() && this.tip().is(':visible')) { - this.hide(); - } else { - this.show(closeAll); - } - }, - - /* - Updates the position of container when content changed. - @method setPosition() - */ - setPosition: function() { - //tbd in child class - }, - - save: function(e, params) { - /** - Fired when new value was submitted. You can use $(this).data('editableContainer') inside handler to access to editableContainer instance - - @event save - @param {Object} event event object - @param {Object} params additional params - @param {mixed} params.newValue submitted value - @param {Object} params.response ajax response - @example - $('#username').on('save', function(e, params) { - //assuming server response: '{success: true}' - var pk = $(this).data('editableContainer').options.pk; - if(params.response && params.response.success) { - alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!'); - } else { - alert('error!'); - } - }); - **/ - this.$element.triggerHandler('save', params); - - //hide must be after trigger, as saving value may require methods of plugin, applied to input - this.hide('save'); - }, - - /** - Sets new option - - @method option(key, value) - @param {string} key - @param {mixed} value - **/ - option: function(key, value) { - this.options[key] = value; - if(key in this.containerOptions) { - this.containerOptions[key] = value; - this.setContainerOption(key, value); - } else { - this.formOptions[key] = value; - if(this.$form) { - this.$form.editableform('option', key, value); - } - } - }, - - setContainerOption: function(key, value) { - this.call('option', key, value); - }, - - /** - Destroys the container instance - @method destroy() - **/ - destroy: function() { - this.hide(); - this.innerDestroy(); - this.$element.off('destroyed'); - this.$element.removeData('editableContainer'); - }, - - /* to be overwritten in child classes */ - innerDestroy: function() { - - }, - - /* - Closes other containers except one related to passed element. - Other containers can be cancelled or submitted (depends on onblur option) - */ - closeOthers: function(element) { - $('.editable-open').each(function(i, el){ - //do nothing with passed element and it's children - if(el === element || $(el).find(element).length) { - return; - } - - //otherwise cancel or submit all open containers - var $el = $(el), - ec = $el.data('editableContainer'); - - if(!ec) { - return; - } - - if(ec.options.onblur === 'cancel') { - $el.data('editableContainer').hide('onblur'); - } else if(ec.options.onblur === 'submit') { - $el.data('editableContainer').tip().find('form').submit(); - } - }); - - }, - - /** - Activates input of visible container (e.g. set focus) - @method activate() - **/ - activate: function() { - if(this.tip && this.tip().is(':visible') && this.$form) { - this.$form.data('editableform').input.activate(); - } - } - - }; - - /** - jQuery method to initialize editableContainer. - - @method $().editableContainer(options) - @params {Object} options - @example - $('#edit').editableContainer({ - type: 'text', - url: '/post', - pk: 1, - value: 'hello' - }); - **/ - $.fn.editableContainer = function (option) { - var args = arguments; - return this.each(function () { - var $this = $(this), - dataKey = 'editableContainer', - data = $this.data(dataKey), - options = typeof option === 'object' && option, - Constructor = (options.mode === 'inline') ? Inline : Popup; - - if (!data) { - $this.data(dataKey, (data = new Constructor(this, options))); - } - - if (typeof option === 'string') { //call method - data[option].apply(data, Array.prototype.slice.call(args, 1)); - } - }); - }; - - //store constructors - $.fn.editableContainer.Popup = Popup; - $.fn.editableContainer.Inline = Inline; - - //defaults - $.fn.editableContainer.defaults = { - /** - Initial value of form input - - @property value - @type mixed - @default null - @private - **/ - value: null, - /** - Placement of container relative to element. Can be top|right|bottom|left. Not used for inline container. - - @property placement - @type string - @default 'top' - **/ - placement: 'top', - /** - Whether to hide container on save/cancel. - - @property autohide - @type boolean - @default true - @private - **/ - autohide: true, - /** - Action when user clicks outside the container. Can be cancel|submit|ignore. - Setting ignore allows to have several containers open. - - @property onblur - @type string - @default 'cancel' - @since 1.1.1 - **/ - onblur: 'cancel', - - /** - Animation speed (inline mode only) - @property anim - @type string - @default false - **/ - anim: false, - - /** - Mode of editable, can be `popup` or `inline` - - @property mode - @type string - @default 'popup' - @since 1.4.0 - **/ - mode: 'popup' - }; - - /* - * workaround to have 'destroyed' event to destroy popover when element is destroyed - * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom - */ - jQuery.event.special.destroyed = { - remove: function(o) { - if (o.handler) { - o.handler(); - } - } - }; - -}(window.jQuery)); - -/** -* Editable Inline -* --------------------- -*/ -(function ($) { - "use strict"; - - //copy prototype from EditableContainer - //extend methods - $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { - containerName: 'editableform', - innerCss: '.editable-inline', - containerClass: 'editable-container editable-inline', //css class applied to container element - - initContainer: function(){ - //container is element - this.$tip = $(''); - - //convert anim to miliseconds (int) - if(!this.options.anim) { - this.options.anim = 0; - } - }, - - splitOptions: function() { - //all options are passed to form - this.containerOptions = {}; - this.formOptions = this.options; - }, - - tip: function() { - return this.$tip; - }, - - innerShow: function () { - this.$element.hide(); - this.tip().insertAfter(this.$element).show(); - }, - - innerHide: function () { - this.$tip.hide(this.options.anim, $.proxy(function() { - this.$element.show(); - this.innerDestroy(); - }, this)); - }, - - innerDestroy: function() { - if(this.tip()) { - this.tip().empty().remove(); - } - } - }); - -}(window.jQuery)); -/** -Makes editable any HTML element on the page. Applied as jQuery method. - -@class editable -@uses editableContainer -**/ -(function ($) { - "use strict"; - - var Editable = function (element, options) { - this.$element = $(element); - //data-* has more priority over js options: because dynamically created elements may change data-* - this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element)); - if(this.options.selector) { - this.initLive(); - } else { - this.init(); - } - - //check for transition support - if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { - this.options.highlight = false; - } - }; - - Editable.prototype = { - constructor: Editable, - init: function () { - var isValueByText = false, - doAutotext, finalize; - - //name - this.options.name = this.options.name || this.$element.attr('id'); - - //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select) - //also we set scope option to have access to element inside input specific callbacks (e. g. source as function) - this.options.scope = this.$element[0]; - this.input = $.fn.editableutils.createInput(this.options); - if(!this.input) { - return; - } - - //set value from settings or by element's text - if (this.options.value === undefined || this.options.value === null) { - this.value = this.input.html2value($.trim(this.$element.html())); - isValueByText = true; - } else { - /* - value can be string when received from 'data-value' attribute - for complext objects value can be set as json string in data-value attribute, - e.g. data-value="{city: 'Moscow', street: 'Lenina'}" - */ - this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); - if(typeof this.options.value === 'string') { - this.value = this.input.str2value(this.options.value); - } else { - this.value = this.options.value; - } - } - - //add 'editable' class to every editable element - this.$element.addClass('editable'); - - //specifically for "textarea" add class .editable-pre-wrapped to keep linebreaks - if(this.input.type === 'textarea') { - this.$element.addClass('editable-pre-wrapped'); - } - - //attach handler activating editable. In disabled mode it just prevent default action (useful for links) - if(this.options.toggle !== 'manual') { - this.$element.addClass('editable-click'); - this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ - //prevent following link if editable enabled - if(!this.options.disabled) { - e.preventDefault(); - } - - //stop propagation not required because in document click handler it checks event target - //e.stopPropagation(); - - if(this.options.toggle === 'mouseenter') { - //for hover only show container - this.show(); - } else { - //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener - var closeAll = (this.options.toggle !== 'click'); - this.toggle(closeAll); - } - }, this)); - } else { - this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually - } - - //if display is function it's far more convinient to have autotext = always to render correctly on init - //see https://github.com/vitalets/x-editable-yii/issues/34 - if(typeof this.options.display === 'function') { - this.options.autotext = 'always'; - } - - //check conditions for autotext: - switch(this.options.autotext) { - case 'always': - doAutotext = true; - break; - case 'auto': - //if element text is empty and value is defined and value not generated by text --> run autotext - doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText; - break; - default: - doAutotext = false; - } - - //depending on autotext run render() or just finilize init - $.when(doAutotext ? this.render() : true).then($.proxy(function() { - if(this.options.disabled) { - this.disable(); - } else { - this.enable(); - } - /** - Fired when element was initialized by `$().editable()` method. - Please note that you should setup `init` handler **before** applying `editable`. - - @event init - @param {Object} event event object - @param {Object} editable editable instance (as here it cannot accessed via data('editable')) - @since 1.2.0 - @example - $('#username').on('init', function(e, editable) { - alert('initialized ' + editable.options.name); - }); - $('#username').editable(); - **/ - this.$element.triggerHandler('init', this); - }, this)); - }, - - /* - Initializes parent element for live editables - */ - initLive: function() { - //store selector - var selector = this.options.selector; - //modify options for child elements - this.options.selector = false; - this.options.autotext = 'never'; - //listen toggle events - this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ - var $target = $(e.target); - if(!$target.data('editable')) { - //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user) - //see https://github.com/vitalets/x-editable/issues/137 - if($target.hasClass(this.options.emptyclass)) { - $target.empty(); - } - $target.editable(this.options).trigger(e); - } - }, this)); - }, - - /* - Renders value into element's text. - Can call custom display method from options. - Can return deferred object. - @method render() - @param {mixed} response server response (if exist) to pass into display function - */ - render: function(response) { - //do not display anything - if(this.options.display === false) { - return; - } - - //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded - if(this.input.value2htmlFinal) { - return this.input.value2html(this.value, this.$element[0], this.options.display, response); - //if display method defined --> use it - } else if(typeof this.options.display === 'function') { - return this.options.display.call(this.$element[0], this.value, response); - //else use input's original value2html() method - } else { - return this.input.value2html(this.value, this.$element[0]); - } - }, - - /** - Enables editable - @method enable() - **/ - enable: function() { - this.options.disabled = false; - this.$element.removeClass('editable-disabled'); - this.handleEmpty(this.isEmpty); - if(this.options.toggle !== 'manual') { - if(this.$element.attr('tabindex') === '-1') { - this.$element.removeAttr('tabindex'); - } - } - }, - - /** - Disables editable - @method disable() - **/ - disable: function() { - this.options.disabled = true; - this.hide(); - this.$element.addClass('editable-disabled'); - this.handleEmpty(this.isEmpty); - //do not stop focus on this element - this.$element.attr('tabindex', -1); - }, - - /** - Toggles enabled / disabled state of editable element - @method toggleDisabled() - **/ - toggleDisabled: function() { - if(this.options.disabled) { - this.enable(); - } else { - this.disable(); - } - }, - - /** - Sets new option - - @method option(key, value) - @param {string|object} key option name or object with several options - @param {mixed} value option new value - @example - $('.editable').editable('option', 'pk', 2); - **/ - option: function(key, value) { - //set option(s) by object - if(key && typeof key === 'object') { - $.each(key, $.proxy(function(k, v){ - this.option($.trim(k), v); - }, this)); - return; - } - - //set option by string - this.options[key] = value; - - //disabled - if(key === 'disabled') { - return value ? this.disable() : this.enable(); - } - - //value - if(key === 'value') { - this.setValue(value); - } - - //transfer new option to container! - if(this.container) { - this.container.option(key, value); - } - - //pass option to input directly (as it points to the same in form) - if(this.input.option) { - this.input.option(key, value); - } - - }, - - /* - * set emptytext if element is empty - */ - handleEmpty: function (isEmpty) { - //do not handle empty if we do not display anything - if(this.options.display === false) { - return; - } - - /* - isEmpty may be set directly as param of method. - It is required when we enable/disable field and can't rely on content - as node content is text: "Empty" that is not empty %) - */ - if(isEmpty !== undefined) { - this.isEmpty = isEmpty; - } else { - //detect empty - //for some inputs we need more smart check - //e.g. wysihtml5 may have
,

, - if(typeof(this.input.isEmpty) === 'function') { - this.isEmpty = this.input.isEmpty(this.$element); - } else { - this.isEmpty = $.trim(this.$element.html()) === ''; - } - } - - //emptytext shown only for enabled - if(!this.options.disabled) { - if (this.isEmpty) { - this.$element.html(this.options.emptytext); - if(this.options.emptyclass) { - this.$element.addClass(this.options.emptyclass); - } - } else if(this.options.emptyclass) { - this.$element.removeClass(this.options.emptyclass); - } - } else { - //below required if element disable property was changed - if(this.isEmpty) { - this.$element.empty(); - if(this.options.emptyclass) { - this.$element.removeClass(this.options.emptyclass); - } - } - } - }, - - /** - Shows container with form - @method show() - @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. - **/ - show: function (closeAll) { - if(this.options.disabled) { - return; - } - - //init editableContainer: popover, tooltip, inline, etc.. - if(!this.container) { - var containerOptions = $.extend({}, this.options, { - value: this.value, - input: this.input //pass input to form (as it is already created) - }); - this.$element.editableContainer(containerOptions); - //listen `save` event - this.$element.on("save.internal", $.proxy(this.save, this)); - this.container = this.$element.data('editableContainer'); - } else if(this.container.tip().is(':visible')) { - return; - } - - //show container - this.container.show(closeAll); - }, - - /** - Hides container with form - @method hide() - **/ - hide: function () { - if(this.container) { - this.container.hide(); - } - }, - - /** - Toggles container visibility (show / hide) - @method toggle() - @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. - **/ - toggle: function(closeAll) { - if(this.container && this.container.tip().is(':visible')) { - this.hide(); - } else { - this.show(closeAll); - } - }, - - /* - * called when form was submitted - */ - save: function(e, params) { - //mark element with unsaved class if needed - if(this.options.unsavedclass) { - /* - Add unsaved css to element if: - - url is not user's function - - value was not sent to server - - params.response === undefined, that means data was not sent - - value changed - */ - var sent = false; - sent = sent || typeof this.options.url === 'function'; - sent = sent || this.options.display === false; - sent = sent || params.response !== undefined; - sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); - - if(sent) { - this.$element.removeClass(this.options.unsavedclass); - } else { - this.$element.addClass(this.options.unsavedclass); - } - } - - //highlight when saving - if(this.options.highlight) { - var $e = this.$element, - bgColor = $e.css('background-color'); - - $e.css('background-color', this.options.highlight); - setTimeout(function(){ - if(bgColor === 'transparent') { - bgColor = ''; - } - $e.css('background-color', bgColor); - $e.addClass('editable-bg-transition'); - setTimeout(function(){ - $e.removeClass('editable-bg-transition'); - }, 1700); - }, 10); - } - - //set new value - this.setValue(params.newValue, false, params.response); - - /** - Fired when new value was submitted. You can use $(this).data('editable') to access to editable instance - - @event save - @param {Object} event event object - @param {Object} params additional params - @param {mixed} params.newValue submitted value - @param {Object} params.response ajax response - @example - $('#username').on('save', function(e, params) { - alert('Saved value: ' + params.newValue); - }); - **/ - //event itself is triggered by editableContainer. Description here is only for documentation - }, - - validate: function () { - if (typeof this.options.validate === 'function') { - return this.options.validate.call(this, this.value); - } - }, - - /** - Sets new value of editable - @method setValue(value, convertStr) - @param {mixed} value new value - @param {boolean} convertStr whether to convert value from string to internal format - **/ - setValue: function(value, convertStr, response) { - if(convertStr) { - this.value = this.input.str2value(value); - } else { - this.value = value; - } - if(this.container) { - this.container.option('value', this.value); - } - $.when(this.render(response)) - .then($.proxy(function() { - this.handleEmpty(); - }, this)); - }, - - /** - Activates input of visible container (e.g. set focus) - @method activate() - **/ - activate: function() { - if(this.container) { - this.container.activate(); - } - }, - - /** - Removes editable feature from element - @method destroy() - **/ - destroy: function() { - this.disable(); - - if(this.container) { - this.container.destroy(); - } - - this.input.destroy(); - - if(this.options.toggle !== 'manual') { - this.$element.removeClass('editable-click'); - this.$element.off(this.options.toggle + '.editable'); - } - - this.$element.off("save.internal"); - - this.$element.removeClass('editable editable-open editable-disabled'); - this.$element.removeData('editable'); - } - }; - - /* EDITABLE PLUGIN DEFINITION - * ======================= */ - - /** - jQuery method to initialize editable element. - - @method $().editable(options) - @params {Object} options - @example - $('#username').editable({ - type: 'text', - url: '/post', - pk: 1 - }); - **/ - $.fn.editable = function (option) { - //special API methods returning non-jquery object - var result = {}, args = arguments, datakey = 'editable'; - switch (option) { - /** - Runs client-side validation for all matched editables - - @method validate() - @returns {Object} validation errors map - @example - $('#username, #fullname').editable('validate'); - // possible result: - { - username: "username is required", - fullname: "fullname should be minimum 3 letters length" - } - **/ - case 'validate': - this.each(function () { - var $this = $(this), data = $this.data(datakey), error; - if (data && (error = data.validate())) { - result[data.options.name] = error; - } - }); - return result; - - /** - Returns current values of editable elements. - Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. - If value of some editable is `null` or `undefined` it is excluded from result object. - When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. - - @method getValue() - @param {bool} isSingle whether to return just value of single element - @returns {Object} object of element names and values - @example - $('#username, #fullname').editable('getValue'); - //result: - { - username: "superuser", - fullname: "John" - } - //isSingle = true - $('#username').editable('getValue', true); - //result "superuser" - **/ - case 'getValue': - if(arguments.length === 2 && arguments[1] === true) { //isSingle = true - result = this.eq(0).data(datakey).value; - } else { - this.each(function () { - var $this = $(this), data = $this.data(datakey); - if (data && data.value !== undefined && data.value !== null) { - result[data.options.name] = data.input.value2submit(data.value); - } - }); - } - return result; - - /** - This method collects values from several editable elements and submit them all to server. - Internally it runs client-side validation for all fields and submits only in case of success. - See
creating new records for details. - Since 1.5.1 `submit` can be applied to single element to send data programmatically. In that case - `url`, `success` and `error` is taken from initial options and you can just call `$('#username').editable('submit')`. - - @method submit(options) - @param {object} options - @param {object} options.url url to submit data - @param {object} options.data additional data to submit - @param {object} options.ajaxOptions additional ajax options - @param {function} options.error(obj) error handler - @param {function} options.success(obj,config) success handler - @returns {Object} jQuery object - **/ - case 'submit': //collects value, validate and submit to server for creating new record - var config = arguments[1] || {}, - $elems = this, - errors = this.editable('validate'); - - // validation ok - if($.isEmptyObject(errors)) { - var ajaxOptions = {}; - - // for single element use url, success etc from options - if($elems.length === 1) { - var editable = $elems.data('editable'); - //standard params - var params = { - name: editable.options.name || '', - value: editable.input.value2submit(editable.value), - pk: (typeof editable.options.pk === 'function') ? - editable.options.pk.call(editable.options.scope) : - editable.options.pk - }; - - //additional params - if(typeof editable.options.params === 'function') { - params = editable.options.params.call(editable.options.scope, params); - } else { - //try parse json in single quotes (from data-params attribute) - editable.options.params = $.fn.editableutils.tryParseJson(editable.options.params, true); - $.extend(params, editable.options.params); - } - - ajaxOptions = { - url: editable.options.url, - data: params, - type: 'POST' - }; - - // use success / error from options - config.success = config.success || editable.options.success; - config.error = config.error || editable.options.error; - - // multiple elements - } else { - var values = this.editable('getValue'); - - ajaxOptions = { - url: config.url, - data: values, - type: 'POST' - }; - } - - // ajax success callabck (response 200 OK) - ajaxOptions.success = typeof config.success === 'function' ? function(response) { - config.success.call($elems, response, config); - } : $.noop; - - // ajax error callabck - ajaxOptions.error = typeof config.error === 'function' ? function() { - config.error.apply($elems, arguments); - } : $.noop; - - // extend ajaxOptions - if(config.ajaxOptions) { - $.extend(ajaxOptions, config.ajaxOptions); - } - - // extra data - if(config.data) { - $.extend(ajaxOptions.data, config.data); - } - - // perform ajax request - $.ajax(ajaxOptions); - } else { //client-side validation error - if(typeof config.error === 'function') { - config.error.call($elems, errors); - } - } - return this; - } - - //return jquery object - return this.each(function () { - var $this = $(this), - data = $this.data(datakey), - options = typeof option === 'object' && option; - - //for delegated targets do not store `editable` object for element - //it's allows several different selectors. - //see: https://github.com/vitalets/x-editable/issues/312 - if(options && options.selector) { - data = new Editable(this, options); - return; - } - - if (!data) { - $this.data(datakey, (data = new Editable(this, options))); - } - - if (typeof option === 'string') { //call method - data[option].apply(data, Array.prototype.slice.call(args, 1)); - } - }); - }; - - - $.fn.editable.defaults = { - /** - Type of input. Can be text|textarea|select|date|checklist and more - - @property type - @type string - @default 'text' - **/ - type: 'text', - /** - Sets disabled state of editable - - @property disabled - @type boolean - @default false - **/ - disabled: false, - /** - How to toggle editable. Can be click|dblclick|mouseenter|manual. - When set to manual you should manually call show/hide methods of editable. - **Note**: if you call show or toggle inside **click** handler of some DOM element, - you need to apply e.stopPropagation() because containers are being closed on any click on document. - - @example - $('#edit-button').click(function(e) { - e.stopPropagation(); - $('#username').editable('toggle'); - }); - - @property toggle - @type string - @default 'click' - **/ - toggle: 'click', - /** - Text shown when element is empty. - - @property emptytext - @type string - @default 'Empty' - **/ - emptytext: 'Empty', - /** - Allows to automatically set element's text based on it's value. Can be auto|always|never. Useful for select and date. - For example, if dropdown list is {1: 'a', 2: 'b'} and element's value set to 1, it's html will be automatically set to 'a'. - auto - text will be automatically set only if element is empty. - always|never - always(never) try to set element's text. - - @property autotext - @type string - @default 'auto' - **/ - autotext: 'auto', - /** - Initial value of input. If not set, taken from element's text. - Note, that if element's text is empty - text is automatically generated from value and can be customized (see `autotext` option). - For example, to display currency sign: - @example - - - - @property value - @type mixed - @default element's text - **/ - value: null, - /** - Callback to perform custom displaying of value in element's text. - If `null`, default input's display used. - If `false`, no displaying methods will be called, element's text will never change. - Runs under element's scope. - _**Parameters:**_ - - * `value` current value to be displayed - * `response` server response (if display called after ajax submit), since 1.4.0 - - For _inputs with source_ (select, checklist) parameters are different: - - * `value` current value to be displayed - * `sourceData` array of items for current input (e.g. dropdown items) - * `response` server response (if display called after ajax submit), since 1.4.0 - - To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`. - - @property display - @type function|boolean - @default null - @since 1.2.0 - @example - display: function(value, sourceData) { - //display checklist as comma-separated values - var html = [], - checked = $.fn.editableutils.itemsByValue(value, sourceData); - - if(checked.length) { - $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); - $(this).html(html.join(', ')); - } else { - $(this).empty(); - } - } - **/ - display: null, - /** - Css class applied when editable text is empty. - - @property emptyclass - @type string - @since 1.4.1 - @default editable-empty - **/ - emptyclass: 'editable-empty', - /** - Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`). - You may set it to `null` if you work with editables locally and submit them together. - - @property unsavedclass - @type string - @since 1.4.1 - @default editable-unsaved - **/ - unsavedclass: 'editable-unsaved', - /** - If selector is provided, editable will be delegated to the specified targets. - Usefull for dynamically generated DOM elements. - **Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options, - as they actually become editable only after first click. - You should manually set class `editable-click` to these elements. - Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element: - - @property selector - @type string - @since 1.4.1 - @default null - @example -
- - Empty - - Operator -
- - - **/ - selector: null, - /** - Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. - - @property highlight - @type string|boolean - @since 1.4.5 - @default #FFFF80 - **/ - highlight: '#FFFF80' - }; - -}(window.jQuery)); - -/** -AbstractInput - base class for all editable inputs. -It defines interface to be implemented by any input type. -To create your own input you can inherit from this class. - -@class abstractinput -**/ -(function ($) { - "use strict"; - - //types - $.fn.editabletypes = {}; - - var AbstractInput = function () { }; - - AbstractInput.prototype = { - /** - Initializes input - - @method init() - **/ - init: function(type, options, defaults) { - this.type = type; - this.options = $.extend({}, defaults, options); - }, - - /* - this method called before render to init $tpl that is inserted in DOM - */ - prerender: function() { - this.$tpl = $(this.options.tpl); //whole tpl as jquery object - this.$input = this.$tpl; //control itself, can be changed in render method - this.$clear = null; //clear button - this.error = null; //error message, if input cannot be rendered - }, - - /** - Renders input from tpl. Can return jQuery deferred object. - Can be overwritten in child objects - - @method render() - **/ - render: function() { - - }, - - /** - Sets element's html by value. - - @method value2html(value, element) - @param {mixed} value - @param {DOMElement} element - **/ - value2html: function(value, element) { - $(element)[this.options.escape ? 'text' : 'html']($.trim(value)); - }, - - /** - Converts element's html to value - - @method html2value(html) - @param {string} html - @returns {mixed} - **/ - html2value: function(html) { - return $('
').html(html).text(); - }, - - /** - Converts value to string (for internal compare). For submitting to server used value2submit(). - - @method value2str(value) - @param {mixed} value - @returns {string} - **/ - value2str: function(value) { - return value; - }, - - /** - Converts string received from server into value. Usually from `data-value` attribute. - - @method str2value(str) - @param {string} str - @returns {mixed} - **/ - str2value: function(str) { - return str; - }, - - /** - Converts value for submitting to server. Result can be string or object. - - @method value2submit(value) - @param {mixed} value - @returns {mixed} - **/ - value2submit: function(value) { - return value; - }, - - /** - Sets value of input. - - @method value2input(value) - @param {mixed} value - **/ - value2input: function(value) { - this.$input.val(value); - }, - - /** - Returns value of input. Value can be object (e.g. datepicker) - - @method input2value() - **/ - input2value: function() { - return this.$input.val(); - }, - - /** - Activates input. For text it sets focus. - - @method activate() - **/ - activate: function() { - if(this.$input.is(':visible')) { - this.$input.focus(); - } - }, - - /** - Creates input. - - @method clear() - **/ - clear: function() { - this.$input.val(null); - }, - - /** - method to escape html. - **/ - escape: function(str) { - return $('
').text(str).html(); - }, - - /** - attach handler to automatically submit form when value changed (useful when buttons not shown) - **/ - autosubmit: function() { - - }, - - /** - Additional actions when destroying element - **/ - destroy: function() { - }, - - // -------- helper functions -------- - setClass: function() { - if(this.options.inputclass) { - this.$input.addClass(this.options.inputclass); - } - }, - - setAttr: function(attr) { - if (this.options[attr] !== undefined && this.options[attr] !== null) { - this.$input.attr(attr, this.options[attr]); - } - }, - - option: function(key, value) { - this.options[key] = value; - } - - }; - - AbstractInput.defaults = { - /** - HTML template of input. Normally you should not change it. - - @property tpl - @type string - @default '' - **/ - tpl: '', - /** - CSS class automatically applied to input - - @property inputclass - @type string - @default null - **/ - inputclass: null, - - /** - If `true` - html will be escaped in content of element via $.text() method. - If `false` - html will not be escaped, $.html() used. - When you use own `display` function, this option obviosly has no effect. - - @property escape - @type boolean - @since 1.5.0 - @default true - **/ - escape: true, - - //scope for external methods (e.g. source defined as function) - //for internal use only - scope: null, - - //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults) - showbuttons: true - }; - - $.extend($.fn.editabletypes, {abstractinput: AbstractInput}); - -}(window.jQuery)); - -/** -List - abstract class for inputs that have source option loaded from js array or via ajax - -@class list -@extends abstractinput -**/ -(function ($) { - "use strict"; - - var List = function (options) { - - }; - - $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); - - $.extend(List.prototype, { - render: function () { - var deferred = $.Deferred(); - - this.error = null; - this.onSourceReady(function () { - this.renderList(); - deferred.resolve(); - }, function () { - this.error = this.options.sourceError; - deferred.resolve(); - }); - - return deferred.promise(); - }, - - html2value: function (html) { - return null; //can't set value by text - }, - - value2html: function (value, element, display, response) { - var deferred = $.Deferred(), - success = function () { - if(typeof display === 'function') { - //custom display method - display.call(element, value, this.sourceData, response); - } else { - this.value2htmlFinal(value, element); - } - deferred.resolve(); - }; - - //for null value just call success without loading source - if(value === null) { - success.call(this); - } else { - this.onSourceReady(success, function () { deferred.resolve(); }); - } - - return deferred.promise(); - }, - - // ------------- additional functions ------------ - - onSourceReady: function (success, error) { - //run source if it function - var source; - if ($.isFunction(this.options.source)) { - source = this.options.source.call(this.options.scope); - this.sourceData = null; - //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed - } else { - source = this.options.source; - } - - //if allready loaded just call success - if(this.options.sourceCache && $.isArray(this.sourceData)) { - success.call(this); - return; - } - - //try parse json in single quotes (for double quotes jquery does automatically) - try { - source = $.fn.editableutils.tryParseJson(source, false); - } catch (e) { - error.call(this); - return; - } - - //loading from url - if (typeof source === 'string') { - //try to get sourceData from cache - if(this.options.sourceCache) { - var cacheID = source, - cache; - - if (!$(document).data(cacheID)) { - $(document).data(cacheID, {}); - } - cache = $(document).data(cacheID); - - //check for cached data - if (cache.loading === false && cache.sourceData) { //take source from cache - this.sourceData = cache.sourceData; - this.doPrepend(); - success.call(this); - return; - } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later - cache.callbacks.push($.proxy(function () { - this.sourceData = cache.sourceData; - this.doPrepend(); - success.call(this); - }, this)); - - //also collecting error callbacks - cache.err_callbacks.push($.proxy(error, this)); - return; - } else { //no cache yet, activate it - cache.loading = true; - cache.callbacks = []; - cache.err_callbacks = []; - } - } - - //ajaxOptions for source. Can be overwritten bt options.sourceOptions - var ajaxOptions = $.extend({ - url: source, - type: 'get', - cache: false, - dataType: 'json', - success: $.proxy(function (data) { - if(cache) { - cache.loading = false; - } - this.sourceData = this.makeArray(data); - if($.isArray(this.sourceData)) { - if(cache) { - //store result in cache - cache.sourceData = this.sourceData; - //run success callbacks for other fields waiting for this source - $.each(cache.callbacks, function () { this.call(); }); - } - this.doPrepend(); - success.call(this); - } else { - error.call(this); - if(cache) { - //run error callbacks for other fields waiting for this source - $.each(cache.err_callbacks, function () { this.call(); }); - } - } - }, this), - error: $.proxy(function () { - error.call(this); - if(cache) { - cache.loading = false; - //run error callbacks for other fields - $.each(cache.err_callbacks, function () { this.call(); }); - } - }, this) - }, this.options.sourceOptions); - - //loading sourceData from server - $.ajax(ajaxOptions); - - } else { //options as json/array - this.sourceData = this.makeArray(source); - - if($.isArray(this.sourceData)) { - this.doPrepend(); - success.call(this); - } else { - error.call(this); - } - } - }, - - doPrepend: function () { - if(this.options.prepend === null || this.options.prepend === undefined) { - return; - } - - if(!$.isArray(this.prependData)) { - //run prepend if it is function (once) - if ($.isFunction(this.options.prepend)) { - this.options.prepend = this.options.prepend.call(this.options.scope); - } - - //try parse json in single quotes - this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true); - - //convert prepend from string to object - if (typeof this.options.prepend === 'string') { - this.options.prepend = {'': this.options.prepend}; - } - - this.prependData = this.makeArray(this.options.prepend); - } - - if($.isArray(this.prependData) && $.isArray(this.sourceData)) { - this.sourceData = this.prependData.concat(this.sourceData); - } - }, - - /* - renders input list - */ - renderList: function() { - // this method should be overwritten in child class - }, - - /* - set element's html by value - */ - value2htmlFinal: function(value, element) { - // this method should be overwritten in child class - }, - - /** - * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}] - */ - makeArray: function(data) { - var count, obj, result = [], item, iterateItem; - if(!data || typeof data === 'string') { - return null; - } - - if($.isArray(data)) { //array - /* - function to iterate inside item of array if item is object. - Caclulates count of keys in item and store in obj. - */ - iterateItem = function (k, v) { - obj = {value: k, text: v}; - if(count++ >= 2) { - return false;// exit from `each` if item has more than one key. - } - }; - - for(var i = 0; i < data.length; i++) { - item = data[i]; - if(typeof item === 'object') { - count = 0; //count of keys inside item - $.each(item, iterateItem); - //case: [{val1: 'text1'}, {val2: 'text2} ...] - if(count === 1) { - result.push(obj); - //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...] - } else if(count > 1) { - //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text') - if(item.children) { - item.children = this.makeArray(item.children); - } - result.push(item); - } - } else { - //case: ['text1', 'text2' ...] - result.push({value: item, text: item}); - } - } - } else { //case: {val1: 'text1', val2: 'text2, ...} - $.each(data, function (k, v) { - result.push({value: k, text: v}); - }); - } - return result; - }, - - option: function(key, value) { - this.options[key] = value; - if(key === 'source') { - this.sourceData = null; - } - if(key === 'prepend') { - this.prependData = null; - } - } - - }); - - List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { - /** - Source data for list. - If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]` - For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order. - - If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option. - - If **function**, it should return data in format above (since 1.4.0). - - Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only). - `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]` - - - @property source - @type string | array | object | function - @default null - **/ - source: null, - /** - Data automatically prepended to the beginning of dropdown list. - - @property prepend - @type string | array | object | function - @default false - **/ - prepend: false, - /** - Error message when list cannot be loaded (e.g. ajax error) - - @property sourceError - @type string - @default Error when loading list - **/ - sourceError: 'Error when loading list', - /** - if true and source is **string url** - results will be cached for fields with the same source. - Usefull for editable column in grid to prevent extra requests. - - @property sourceCache - @type boolean - @default true - @since 1.2.0 - **/ - sourceCache: true, - /** - Additional ajax options to be used in $.ajax() when loading list from server. - Useful to send extra parameters (`data` key) or change request method (`type` key). - - @property sourceOptions - @type object|function - @default null - @since 1.5.0 - **/ - sourceOptions: null - }); - - $.fn.editabletypes.list = List; - -}(window.jQuery)); - -/** -Text input - -@class text -@extends abstractinput -@final -@example -awesome - -**/ -(function ($) { - "use strict"; - - var Text = function (options) { - this.init('text', options, Text.defaults); - }; - - $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput); - - $.extend(Text.prototype, { - render: function() { - this.renderClear(); - this.setClass(); - this.setAttr('placeholder'); - }, - - activate: function() { - if(this.$input.is(':visible')) { - this.$input.focus(); - $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length); - if(this.toggleClear) { - this.toggleClear(); - } - } - }, - - //render clear button - renderClear: function() { - if (this.options.clear) { - this.$clear = $(''); - this.$input.after(this.$clear) - .css('padding-right', 24) - .keyup($.proxy(function(e) { - //arrows, enter, tab, etc - if(~$.inArray(e.keyCode, [40,38,9,13,27])) { - return; - } - - clearTimeout(this.t); - var that = this; - this.t = setTimeout(function() { - that.toggleClear(e); - }, 100); - - }, this)) - .parent().css('position', 'relative'); - - this.$clear.click($.proxy(this.clear, this)); - } - }, - - postrender: function() { - /* - //now `clear` is positioned via css - if(this.$clear) { - //can position clear button only here, when form is shown and height can be calculated -// var h = this.$input.outerHeight(true) || 20, - var h = this.$clear.parent().height(), - delta = (h - this.$clear.height()) / 2; - - //this.$clear.css({bottom: delta, right: delta}); - } - */ - }, - - //show / hide clear button - toggleClear: function(e) { - if(!this.$clear) { - return; - } - - var len = this.$input.val().length, - visible = this.$clear.is(':visible'); - - if(len && !visible) { - this.$clear.show(); - } - - if(!len && visible) { - this.$clear.hide(); - } - }, - - clear: function() { - this.$clear.hide(); - this.$input.val('').focus(); - } - }); - - Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { - /** - @property tpl - @default - **/ - tpl: '', - /** - Placeholder attribute of input. Shown when input is empty. - - @property placeholder - @type string - @default null - **/ - placeholder: null, - - /** - Whether to show `clear` button - - @property clear - @type boolean - @default true - **/ - clear: true - }); - - $.fn.editabletypes.text = Text; - -}(window.jQuery)); - -/** -Textarea input - -@class textarea -@extends abstractinput -@final -@example -awesome comment! - -**/ -(function ($) { - "use strict"; - - var Textarea = function (options) { - this.init('textarea', options, Textarea.defaults); - }; - - $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput); - - $.extend(Textarea.prototype, { - render: function () { - this.setClass(); - this.setAttr('placeholder'); - this.setAttr('rows'); - - //ctrl + enter - this.$input.keydown(function (e) { - if (e.ctrlKey && e.which === 13) { - $(this).closest('form').submit(); - } - }); - }, - - //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant! - /* - value2html: function(value, element) { - var html = '', lines; - if(value) { - lines = value.split("\n"); - for (var i = 0; i < lines.length; i++) { - lines[i] = $('
').text(lines[i]).html(); - } - html = lines.join('
'); - } - $(element).html(html); - }, - - html2value: function(html) { - if(!html) { - return ''; - } - - var regex = new RegExp(String.fromCharCode(10), 'g'); - var lines = html.split(//i); - for (var i = 0; i < lines.length; i++) { - var text = $('
').html(lines[i]).text(); - - // Remove newline characters (\n) to avoid them being converted by value2html() method - // thus adding extra
tags - text = text.replace(regex, ''); - - lines[i] = text; - } - return lines.join("\n"); - }, - */ - activate: function() { - $.fn.editabletypes.text.prototype.activate.call(this); - } - }); - - Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { - /** - @property tpl - @default - **/ - tpl:'', - /** - @property inputclass - @default input-large - **/ - inputclass: 'input-large', - /** - Placeholder attribute of input. Shown when input is empty. - - @property placeholder - @type string - @default null - **/ - placeholder: null, - /** - Number of rows in textarea - - @property rows - @type integer - @default 7 - **/ - rows: 7 - }); - - $.fn.editabletypes.textarea = Textarea; - -}(window.jQuery)); - -/** -Select (dropdown) - -@class select -@extends list -@final -@example - - -**/ -(function ($) { - "use strict"; - - var Select = function (options) { - this.init('select', options, Select.defaults); - }; - - $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); - - $.extend(Select.prototype, { - renderList: function() { - this.$input.empty(); - - var fillItems = function($el, data) { - var attr; - if($.isArray(data)) { - for(var i=0; i', attr), data[i].children)); - } else { - attr.value = data[i].value; - if(data[i].disabled) { - attr.disabled = true; - } - $el.append($('