diff --git a/InvenTree/static/css/bootstrap-editable.css b/InvenTree/static/css/bootstrap-editable.css
new file mode 100644
index 0000000000..eaef0de969
--- /dev/null
+++ b/InvenTree/static/css/bootstrap-editable.css
@@ -0,0 +1,663 @@
+/*! 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/InvenTree/static/script/bootstrap-editable.js b/InvenTree/static/script/bootstrap-editable.js
new file mode 100644
index 0000000000..dd8138568e
--- /dev/null
+++ b/InvenTree/static/script/bootstrap-editable.js
@@ -0,0 +1,6807 @@
+/*! 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|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 $('$().editable()
. You should subscribe on it's events (save / cancel) to get profit of it.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 $(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
+
+
+
+ **/
+ 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 $('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] = $('