diff --git a/.gitignore b/.gitignore index a65c8fb..30bc79f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -node_modules -dist -test/instrumented/ +node_modules +test/instrumented/ *.nupkg \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..958c4b1 --- /dev/null +++ b/bower.json @@ -0,0 +1,16 @@ +{ + "name": "x-editable", + "version": "1.4.4", + "main": "dist/README.md", + "ignore": [ + "*.*", + "LICENSE-MIT", + "node_modules", + "components", + "src", + "test" + ], + "dependencies": { + "jquery": "~1" + } +} \ No newline at end of file diff --git a/dist/CHANGELOG.txt b/dist/CHANGELOG.txt new file mode 100644 index 0000000..3c86e5e --- /dev/null +++ b/dist/CHANGELOG.txt @@ -0,0 +1,181 @@ +X-editable changelog +============================= + + +Version 1.4.4 May 4, 2013 +---------------------------- +[enh #219] added `error` callback (joekaiser) +[enh #198] new value of showbuttons: 'bottom' (vitalets) +[enh #192] add class editable-popup to have diferent css for popup and inline (vitalets) +[enh] update to bootstrap-datepicker 1.0.2 (vitalets) +[enh] update to combodate 1.0.3 with yearDescending and roundTime options (vitalets) +[enh] add 'use strict' directive (vitalets) +[enh #202] allow pk=0 (mdeweerd) +[enh #183] move datepicker icon to center of button (vitalets) +[enh] upgrade to select2 3.3.2 (vitalets) +[enh #176] update to bootstrap 2.3.1 (vitalets) +[bug #171] clear in date & datetime when showbuttons=false (vitalets) +[bug #166] clear button for input type=number (vitalets) +[bug #65] checklist don't show checked for single value (vitalets) +[enh #188] added bootstrap datetime (adeg, vitalets) +[bug] editable-poshytip on inline mode tries to write in $.Poshytip (vitalets) + + +Version 1.4.3 Mar 8, 2013 +---------------------------- +[bug #32] hotfix for jQuery UI 1.9+ (vitalets) + + +Version 1.4.2 Mar 7, 2013 +---------------------------- +[enh #132] combodate options can be defined via data-combodate json string (vitalets) +[enh] source defined as function now has scope of element and can return string used as url (vitalets) +[bug #99] select2 with Hierarchical Data (kev360) +[bug #81] wysihtml5: fix inserting image (vitalets) +[bug] remove $.browser from wysihtml5 input to support jQuery 1.9 (vitalets) +[bug #142] editable poshytip jquery 1.9+ compatibility (spiderpug) +[enh #126] Update bootstrap datepicker library and add minViewMode to options (kev360) +[enh #150] select2 with showbuttons = false (vitalets) +[bug #149] datepicker not shown when showbuttons = false (vitalets) +[bug #133] clear button incorect position due to parent line-height property (vitalets) +[bug #141] data-value ignored for empty elements (vitalets) +[bug #137] fix empty class for delegated element (vitalets) +[enh #121] add support of momentjs 2.0.0 in combodate (vitalets) + + +Version 1.4.1 Jan 18, 2013 +---------------------------- +[enh #62] new option `selector` to work with delegated targets (vitalets) +[enh] new option `unsavedclass` to set css class when value was not sent to server (vitalets) +[enh] new option `emptyclass` to set css class when element is empty (vitalets) +[enh #59] select2 input (vitalets) +[enh #17] typeahead input (vitalets) +[enh] select: support of OPTGROUP via `children` key in source (vitalets) +[enh] checklist: set checked via prop instead of attr (vitalets) + + +Version 1.4.0 Jan 11, 2013 +---------------------------- +[enh] added new input type: combodate (vitalets) +[bug #68] allow arrays for data attributes (adimitrov) +[enh] setValue method updates input if form is open (vitalets) +[enh] select: change source via option method, see #61 (vitalets) +[bug] select: source loaded twice if sourceCache = false (vitalets) +[enh] added `destroy` method, see #61 (vitalets) +[enh] textarea: added `rows` property (vitalets) +[enh #60] added wysihtml5 input (vitalets) +[enh] added IOS-style clear button for text inputs (vitalets) +[enh] date inputs changed in inline mode (vitalets) +[enh #51] popup/inline modes can be toggled via `mode` config option. No more *-inline.js versions of files (vitalets) +[enh] update bootstrap-datepicker to upstream (vitalets) +[enh] 'display' method: added param 'response' allowing to show text directly from server (vitalets) +[enh] new util method `$.fn.editableutils.itemsByValue` to easily get selected items for sourced-inputs (vitalets) +[enh] convert newlines to <br> in error message for more pretty display (vitalets) +[enh #57] remove css height for textarea (vitalets) +[enh] if new value for select is 'null' source should not load (vitalets) +[enh #53] 'name' no more appended to source defined as url (vitalets) +[enh #46] move 'img' dir outside 'css' (vitalets) +[enh #48] fix handling of newlines in textarea input (jmfontaine) +[enh #47] set select source to function (brianchance) +[bug] fix inline container move on next line in IE7 (vitalets) + + +Version 1.3.0 Dec 10, 2012 +---------------------------- +[enh] added html5 inputs support: password, email, url, tel, number, range (vitalets) +[bug #43] fix for bootstrap 2.2.2 (vitalets) +[enh #41] 'abstract' class renamed to 'abstractinput' as abstract is reserved word (vitalets) +[enh #40] 'params' option defined as function overwrites original ajax data instead of appending (vitalets) +[bug] datepicker: error when click on arrows after clear date (vitalets) +[enh] 'hidden' event: added possible value of reason param - 'nochange'. Occurs when form is submitted but value was not changed (vitalets) +[enh] 'submit' method changed: error-callback's parameter simplified (vitalets) +[enh] 'submit' method changed: now when response 200 OK it does not set pk automatically (vitalets) +[enh] 'submit' method changed: removed dataType='json'. Use 'ajaxOptions' to specify dataType if needed (vitalets) +[enh] removed default ajax dataType='json'. Use 'ajaxOptions' to specify dataType if needed (vitalets) +[enh] select: do not show 'sourceError' in element during autotext execution (vitalets) + + +Version 1.2.0 Dec 6, 2012 +---------------------------- +[enh #36] 'submit' method: added 'ajaxOptions' property to modify ajax request (vitalets) +[enh] inputs now internally use 'value2submit' method instead of previous 'value2str' (vitalets) +[enh] editableContainer removed from docs (vitalets) +[enh] editableContainer: removed 'autohide' option and 'cancel' event. Use 'hidden' event instead (vitalets) +[enh] 'hidden' event: added param 'reason' that points to reason caused hiding (vitalets) +[enh] 'select' submit by enter (vitalets) +[bug #37] fix incorrectly shown datepicker in jquery 1.7.1 + webkit (vitalets) +[enh] added url param 'jquery' to run tests in different versions of jquery, e.g. '&jquery=1.7.2' (vitalets) +[enh] 'enablefocus' option removed. More efficient to use 'save/hide' events to set focus to any element (vitalets) +[enh] 'init' event was added due to removal of render event (vitalets) +[enh] 'render' event was removed, use 'display' callback instead (vitalets) +[enh] 'checklist' submit value as array, not comma separated string (vitalets) +[enh] 'checklist' was refactored: options 'viewseparator', 'limit', 'limitText' are supressed by 'display' callback (vitalets) +[enh] new option: 'display' callback. Makes far more flexible rendering value into element's text. (vitalets) +[bug] fix typos (atrophic) +[enh] all callbacks scope changed to element (vitalets) +[enh] new option: 'savenochange' to save or cancel value when it was not changed in form (vitalets) +[enh] composite pk can be defined as JSON in data-pk attribute (vitalets) +[enh #30] new option 'sourceCache' true|false to disable cache for select (vitalets) +[bug #34] inputclass span* broken with fluid bootstrap layout. Classes changed to 'input-*'. (vitalets) +[enh] utils now added to $.fn.editableutils instead of $.fn.editableform.utils (vitalets) +[enh] input types now added to $.fn.editabletypes instead of $.fn.editableform.types (vitalets) +[enh] playground and tests now use requirejs (vitalets) +[bug #27] 'today' button toggle bug in bootstrap-datepicker (vitalets) + + +Version 1.1.1 Nov 30, 2012 +---------------------------- +[enh] 'showbuttons' option to hide buttons in form (vitalets) +[enh] object can be passed in 'option' method to set several options at once (vitalets) +[enh #20] toggle editable by 'dblclick' and 'mouseenter' (vitalets) +[enh] added 'inputs-ext' directory with sample input 'address'. They will not be concatenated to main files (vitalets) +[enh #13] 'onblur' option: to cancel, submit or ignore when user clicks outside the form (vitalets) +[enh] 'ajaxOptions' parameter for advanced ajax configuration (vitalets) +[enh] 'success' callback can return object to overwrite submitted value (vitalets) + + +Version 1.1.0 Nov 27, 2012 +---------------------------- +[enh #11] icon cancel changed to 'cross' (tarciozemel) +[enh] added support for IE7+ (vitalets) +[enh #9] 'name' or 'id' is not required anymore (vitalets) +[enh] 'clear' button added in date and dateui (vitalets) +[enh] form template changed: added DIV.editable-input, DIV.editable.buttons and $.fn.editableform.buttons (vitalets) +[enh] new input type: checklist (vitalets) +[enh] updated docs: inputs dropdown menu, global templates section (vitalets) + + +Version 1.0.1 Nov 22, 2012 +---------------------------- +[enh] contribution guide in README.md (vitalets) +[enh #7] 'shown', 'hidden' events added (vitalets) +[enh #1] params can be a function to calculate it dynamically (vitalets) +[enh #6] do not preventDefault() in click when toggle='manual'. This allows to have clickable links (vitalets) +[bug #3] should not mark element with unsave css if url is user's function (vitalets) + + +Version 1.0.0 Nov 19, 2012 +---------------------------- +Initial release. This library is new life of bootstrap-editable (1.1.4) that was strongly refactored and improved. +Main features: +- support not only bootstrap but any core library: bootstrap, jquery-ui or pure jquery +- different container classes to show form: popover, tooltip, poshytip, etc +- inline and popup versions +- new directory structure and logic in separate js files allowing easy contribution + +It is not fully compatible with bootstrap-editable but has mainly the same interface and api. +Here list of differences to help you to upgrade your application: + +[change] 'toggle' option value can be only click|manual (not toggling element id). In case of 'manual' you should write handler calling 'show' method. +[change] 'validate' option cannot be defined as object anymore. +[change] events 'init', 'update', 'shown', 'hidden' removed. Events 'save', 'cancel' added. Event 'render' remains. +[change] input's option 'template' renamed to 'tpl' (to exclude conflict with container's template). +[change] value can be stored internally as object (previously was always string). Useful for date input. +[change] 'error' callback option is removed. 'success' callback remained. +[enh] 'source' option in select can be array of structure [{value: 1, text: 'abc'}, {...}]. This allows to keep ordering of items in dropdown list. Previous format is supported for compatibility. +[enh] api method 'setValue' to set manually value of editable. +[change] locales directory is excluded from bootstrap-datepicker input. If you need localization you should jus download corresponding file from github. +[change] date and dateui specific options can be set only via 'datepicker' option in first level of config (previously it was possible to set some options directly in config, e.g. weekStart). +[change] if 'url' option defined as function - it is used as submit method instead of ajax (previously it was dynamically return url string and ajax occurred anyway) + +Also all known bugs of bootstrap-editable were closed. diff --git a/dist/LICENSE-MIT b/dist/LICENSE-MIT new file mode 100644 index 0000000..bc67d5b --- /dev/null +++ b/dist/LICENSE-MIT @@ -0,0 +1,22 @@ +Copyright (c) 2012 Vitaliy Potapov + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/dist/README.md b/dist/README.md new file mode 100644 index 0000000..18f0d7e --- /dev/null +++ b/dist/README.md @@ -0,0 +1,71 @@ +# X-editable + +In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery. +It is a new life of [bootstrap-editable plugin](http://github.com/vitalets/bootstrap-editable) that was strongly refactored and improved. + +## Demo + Docs + Download +See **http://vitalets.github.com/x-editable** + +## Reporting issues +When creating issues please provide [jsFiddle](http://jsfiddle.net) example. You can easily fork one of **jsFiddle templates**: +1. [bootstrap](http://jsfiddle.net/xBB5x/195) +2. [jqueryui](http://jsfiddle.net/xBB5x/196) +3. [plain](http://jsfiddle.net/xBB5x/197) +Your feedback is very appreciated! + +## Contribution +A few steps how to start contributing: + +1.[Fork X-editable](https://github.com/vitalets/x-editable/fork) and pull the latest changes from <code>dev</code> branch + +2.Arrange local directory structure. It should be: +**x-editable** + | -- **lib** (repo related to <code>dev</code> and <code>master</code> branches) + | -- **gh-pages** (repo related to <code>gh-pages</code> branch for docs & demo) + | -- **playground** (simple node-server and html page for testing, [playground_1.2.zip](https://github.com/downloads/vitalets/x-editable/playground_1.2.zip), **updated in 1.2.0!**) + +To make it easy follow this script ( _assuming you have [nodejs](http://nodejs.org) installed_ ). +Please replace <code><your-github-name></code> with your name: +```` +mkdir x-editable +cd x-editable + +#lib +git clone https://github.com/<your-github-name>/x-editable.git -b dev lib +cd lib +#install gruntjs globally - building tool +npm install -g grunt +#install other dependencies - grunt-contrib +npm install +cd .. + +#gh-pages +git clone https://github.com/<your-github-name>/x-editable.git -b gh-pages gh-pages +cd gh-pages +npm install +cd .. + +#playground +#download playground.zip from https://github.com/downloads/vitalets/x-editable/playground_1.2.zip +unzip playground.zip +cd playground +npm install +```` +3.That's it! You can start editing files in **lib/src** directory or create new editable input/container/whatever. +To test the result go to **playground**, start server <code>node server.js</code> and open in your browser [http://localhost:3000/playground](http://localhost:3000/playground). + +4.To run unit tests you can open it directly in browser **lib/test/index.html**. +Or use grunt's _qunit_ task <code>grunt test</code>. For that you also need to [install PhantomJS](https://github.com/gruntjs/grunt/blob/master/docs/faq.md#why-does-grunt-complain-that-phantomjs-isnt-installed) + +5.To build distributive run <code>grunt build</code> in **lib** directory. Result will appear in **lib/dist**. + +6.To build docs run <code>build data-docs-dist</code> in **gh-pages** directory. Result will appear in **gh-pages/*.html**. +Do not edit **index.html** and **docs.html** directly! Instead look at [Handlebars](https://github.com/wycats/handlebars.js) templates in **generator/templates**. + +7.Commit changes on <code>dev</code> / <code>gh-pages-dev</code> branch and make pull request as usual. + +Thanks for your support! + +## License +Copyright (c) 2012 Vitaliy Potapov +Licensed under the MIT licenses. \ No newline at end of file diff --git a/dist/bootstrap-editable/css/bootstrap-editable.css b/dist/bootstrap-editable/css/bootstrap-editable.css new file mode 100644 index 0000000..32b713f --- /dev/null +++ b/dist/bootstrap-editable/css/bootstrap-editable.css @@ -0,0 +1,495 @@ +/*! X-editable - v1.4.4 +* 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 */ +} + +.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-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 */ +} +.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: '*'*/ +} + +/*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 !important; +} +.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.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 { + color: #999999; +} +.datepicker th.switch { + width: 145px; +} +.datepicker thead tr:first-child th, +.datepicker tfoot tr:first-child th { + cursor: pointer; +} +.datepicker thead tr:first-child th:hover, +.datepicker tfoot tr:first-child 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; +} diff --git a/dist/bootstrap-editable/img/clear.png b/dist/bootstrap-editable/img/clear.png new file mode 100644 index 0000000..580b52a Binary files /dev/null and b/dist/bootstrap-editable/img/clear.png differ diff --git a/dist/bootstrap-editable/img/loading.gif b/dist/bootstrap-editable/img/loading.gif new file mode 100644 index 0000000..5b33f7e Binary files /dev/null and b/dist/bootstrap-editable/img/loading.gif differ diff --git a/dist/bootstrap-editable/js/bootstrap-editable.js b/dist/bootstrap-editable/js/bootstrap-editable.js new file mode 100644 index 0000000..b924383 --- /dev/null +++ b/dist/bootstrap-editable/js/bootstrap-editable.js @@ -0,0 +1,6192 @@ +/*! X-editable - v1.4.4 +* 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); + }, + 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(); + + /** + Fired when rendering starts + @event rendering + @param {Object} event event object + **/ + this.$div.triggerHandler('rendering'); + + //init input + this.initInput(); + + //append input to form + this.input.prerender(); + 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'); + this.input.value2input(this.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 <br> for more pretty error display + if(msg) { + lines = msg.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('<div>').text(lines[i]).html(); + } + msg = lines.join('<br>'); + } + $group.addClass($.fn.editableform.errorGroupClass); + $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); + } + }, + + submit: function(e) { + e.stopPropagation(); + e.preventDefault(); + + var error, + newValue = this.input.input2value(); //get new value from input + + //validation + if (error = this.validate(newValue)) { + 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; + } + + //sending data to server + $.when(this.save(newValue)) + .done($.proxy(function(response) { + //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: <something>} --> 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 submitted value + @param {Object} params.response ajax response + + @example + $('#form-div').on('save'), function(e, params){ + if(params.newValue === 'username') {...} + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, response: response}); + }, this)) + .fail($.proxy(function(xhr) { + 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(newValue) { + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + //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 = !!(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 <code>text|textarea|select|date|checklist</code> + + @property type + @type string + @default 'text' + **/ + type: 'text', + /** + Url for submit, e.g. <code>'/post'</code> + 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 <code>object</code> - it is **appended** to original ajax data (pk, name and value). + If defined as <code>function</code> - 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 <code>id</code> 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. <code>{id: 1, lang: 'en'}</code>. + 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, + /** + Strategy for sending data on server. Can be <code>auto|always|never</code>. + When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element. + + @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. + + @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 <code>{success: true}</code> + or <code>{success: false, msg: "server error"}</code> 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** <code>{newValue: <something>}</code> - 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 <code>null</code> 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 = '<form class="form-inline editableform">'+ + '<div class="control-group">' + + '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+ + '<div class="editable-error-block"></div>' + + '</div>' + + '</form>'; + + //loading div + $.fn.editableform.loading = '<div class="editableform-loading"></div>'; + + //buttons + $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+ + '<button type="button" class="editable-cancel">cancel</button>'; + + //error class attached to control-group + $.fn.editableform.errorGroupClass = null; + + //error class attached to editable-error-block + $.fn.editableform.errorBlockClass = 'editable-error'; +}(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: <a data-source="{'a': 'b', 'c': 'd'}"> + * 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 $('<div>').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 []; + } + + valueProp = valueProp || 'value'; + + 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' ? o[valueProp] : o); }).length) { + result.push(o); + } + } else { + if(value == (o && typeof o === 'object' ? o[valueProp] : o)) { + 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; + } + } + + }; +}(window.jQuery)); + +/** +Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br> +This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br> +Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br> +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, //tbd in child class + innerCss: null, //tbd in child class + containerClass: 'editable-container editable-popup', //css class applied to container element + 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(); + + //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<exclude_classes.length; i++) { + if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) { + return; + } + } + + //close all open containers (except one - target) + Popup.prototype.closeOthers(e.target); + }); + + $(document).data('editable-handlers-attached', true); + } + }, + + //split options on containerOptions and formOptions + splitOptions: function() { + this.containerOptions = {}; + this.formOptions = {}; + + if(!$.fn[this.containerName]) { + throw new Error(this.containerName + ' not found. Have you included corresponding js file?'); + } + + var cDef = $.fn[this.containerName].defaults; + //keys defined in container defaults go to container, others go to form + for(var k in this.options) { + if(k in cDef) { + this.containerOptions[k] = this.options[k]; + } else { + this.formOptions[k] = this.options[k]; + } + } + }, + + /* + Returns jquery object of container + @method tip() + */ + tip: function() { + return this.container() ? this.container().$tip : null; + }, + + /* returns container object */ + container: function() { + return this.$element.data(this.containerDataName || this.containerName); + }, + + /* call native method of underlying container, e.g. this.$element.popover('method') */ + call: function() { + this.$element[this.containerName].apply(this.$element, arguments); + }, + + initContainer: function(){ + this.call(this.containerOptions); + }, + + renderForm: function() { + this.$form + .editableform(this.formOptions) + .on({ + save: $.proxy(this.save, this), //click on submit button (value changed) + nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed) + cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button + show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state) + rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown + resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed + rendered: $.proxy(function(){ + /** + Fired when container is shown and form is rendered (for select will wait for loading dropdown options). + **Note:** Bootstrap popover has own `shown` 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 shown + @param {Object} event event object + @example + $('#username').on('shown', function(e, editable) { + editable.input.$input.val('overwriting value of input..'); + }); + **/ + /* + TODO: added second param mainly to distinguish from bootstrap's shown event. It's a hotfix that will be solved in future versions via namespaced events. + */ + this.$element.triggerHandler('shown', this); + }, this) + }) + .editableform('render'); + }, + + /** + Shows container with form + @method show() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + /* Note: poshytip owerwrites this method totally! */ + show: function (closeAll) { + this.$element.addClass('editable-open'); + if(closeAll !== false) { + //close all open containers (except this) + this.closeOthers(this.$element[0]); + } + + //show container itself + this.innerShow(); + this.tip().addClass(this.containerClass); + + /* + Currently, form is re-rendered on every show. + The main reason is that we dont know, what container will do with content when closed: + remove(), detach() or just hide(). + + Detaching form itself before hide and re-insert before show is good solution, + but visually it looks ugly, as container changes size before hide. + */ + + //if form already exist - delete previous data + if(this.$form) { + //todo: destroy prev data! + //this.$form.destroy(); + } + + this.$form = $('<div>'); + + //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 <code>save|cancel|onblur|nochange|undefined (=manual)</code> + **/ + hide: function(reason) { + if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { + return; + } + + 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 <code>save|cancel|onblur|nochange|manual</code> + @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 <code>$(this).data('editableContainer')</code> 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 od 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 <code>top|right|bottom|left</code>. 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 <code>cancel|submit|ignore</code>. + Setting <code>ignore</code> allows to have several containers open. + + @property onblur + @type string + @default 'cancel' + @since 1.1.1 + **/ + onblur: 'cancel', + + /** + Animation speed (inline mode) + @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 <span> element + this.$tip = $('<span></span>'); + + //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(); + } + }; + + 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'); + + //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 + 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 + } + + //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; + } + + this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === ''; + + //emptytext shown only for enabled + if(!this.options.disabled) { + if (this.isEmpty) { + this.$element.text(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); + } + } + + //set new value + this.setValue(params.newValue, false, params.response); + + /** + Fired when new value was submitted. You can use <code>$(this).data('editable')</code> 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(); + } + + 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. + + @method getValue() + @returns {Object} object of element names and values + @example + $('#username, #fullname').editable('getValue'); + // possible result: + { + username: "superuser", + fullname: "John" + } + **/ + case 'getValue': + 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 <a href="#newrecord">creating new records</a> for details. + + @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'), + values; + + if($.isEmptyObject(errors)) { + values = this.editable('getValue'); + if(config.data) { + $.extend(values, config.data); + } + + $.ajax($.extend({ + url: config.url, + data: values, + type: 'POST' + }, config.ajaxOptions)) + .success(function(response) { + //successful response 200 OK + if(typeof config.success === 'function') { + config.success.call($elems, response, config); + } + }) + .error(function(){ //ajax error + if(typeof config.error === 'function') { + config.error.apply($elems, arguments); + } + }); + } 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; + + 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 <code>text|textarea|select|date|checklist</code> 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 <code>click|dblclick|mouseenter|manual</code>. + When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable. + **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element, + you need to apply <code>e.stopPropagation()</code> 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 <code>auto|always|never</code>. Useful for select and date. + For example, if dropdown list is <code>{1: 'a', 2: 'b'}</code> and element's value set to <code>1</code>, it's html will be automatically set to <code>'a'</code>. + <code>auto</code> - text will be automatically set only if element is empty. + <code>always|never</code> - 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 + <a id="price" data-type="text" data-value="100"></a> + <script> + $('#price').editable({ + ... + display: function(value) { + $(this).text(value + '$'); + } + }) + </script> + + @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 + <div id="user"> + <!-- empty --> + <a href="#" data-name="username" data-type="text" class="editable-click editable-empty" data-value="" title="Username">Empty</a> + <!-- non-empty --> + <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" class="editable-click" title="Group">Operator</a> + </div> + + <script> + $('#user').editable({ + selector: 'a', + url: '/post', + pk: 1 + }); + </script> + **/ + selector: null + }; + +}(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).text(value); + }, + + /** + Converts element's html to value + + @method html2value(html) + @param {string} html + @returns {mixed} + **/ + html2value: function(html) { + return $('<div>').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 $('<div>').text(str).html(); + }, + + /** + attach handler to automatically submit form when value changed (useful when buttons not shown) + **/ + autosubmit: 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 input-medium + **/ + inputclass: 'input-medium', + //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) { + //if allready loaded just call success + if($.isArray(this.sourceData)) { + success.call(this); + return; + } + + // try parse json in single quotes (for double quotes jquery does automatically) + try { + this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false); + } catch (e) { + error.call(this); + return; + } + + var source = this.options.source; + + //run source if it function + if ($.isFunction(source)) { + source = source.call(this.options.scope); + } + + //loading from url + if (typeof source === 'string') { + //try to get 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 = []; + } + } + + //loading sourceData from server + $.ajax({ + 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) + }); + } 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 <code>true</code> 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 + }); + + $.fn.editabletypes.list = List; + +}(window.jQuery)); + +/** +Text input + +@class text +@extends abstractinput +@final +@example +<a href="#" id="username" data-type="text" data-pk="1">awesome</a> +<script> +$(function(){ + $('#username').editable({ + url: '/post', + title: 'Enter username' + }); +}); +</script> +**/ +(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 = $('<span class="editable-clear-x"></span>'); + 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 <input type="text"> + **/ + tpl: '<input type="text">', + /** + 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 +<a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a> +<script> +$(function(){ + $('#comments').editable({ + url: '/post', + title: 'Enter comments', + rows: 10 + }); +}); +</script> +**/ +(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(); + } + }); + }, + + value2html: function(value, element) { + var html = '', lines; + if(value) { + lines = value.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('<div>').text(lines[i]).html(); + } + html = lines.join('<br>'); + } + $(element).html(html); + }, + + html2value: function(html) { + if(!html) { + return ''; + } + + var regex = new RegExp(String.fromCharCode(10), 'g'); + var lines = html.split(/<br\s*\/?>/i); + for (var i = 0; i < lines.length; i++) { + var text = $('<div>').html(lines[i]).text(); + + // Remove newline characters (\n) to avoid them being converted by value2html() method + // thus adding extra <br> 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 <textarea></textarea> + **/ + tpl:'<textarea></textarea>', + /** + @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 +<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a> +<script> +$(function(){ + $('#status').editable({ + value: 2, + source: [ + {value: 1, text: 'Active'}, + {value: 2, text: 'Blocked'}, + {value: 3, text: 'Deleted'} + ] + }); +}); +</script> +**/ +(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) { + if($.isArray(data)) { + for(var i=0; i<data.length; i++) { + if(data[i].children) { + $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children)); + } else { + $el.append($('<option>', {value: data[i].value}).text(data[i].text)); + } + } + } + return $el; + }; + + fillItems(this.$input, this.sourceData); + + this.setClass(); + + //enter submit + this.$input.on('keydown.editable', function (e) { + if (e.which === 13) { + $(this).closest('form').submit(); + } + }); + }, + + value2htmlFinal: function(value, element) { + var text = '', + items = $.fn.editableutils.itemsByValue(value, this.sourceData); + + if(items.length) { + text = items[0].text; + } + + $(element).text(text); + }, + + autosubmit: function() { + this.$input.off('keydown.editable').on('change.editable', function(){ + $(this).closest('form').submit(); + }); + } + }); + + Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { + /** + @property tpl + @default <select></select> + **/ + tpl:'<select></select>' + }); + + $.fn.editabletypes.select = Select; + +}(window.jQuery)); + +/** +List of checkboxes. +Internally value stored as javascript array of values. + +@class checklist +@extends list +@final +@example +<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-original-title="Select options"></a> +<script> +$(function(){ + $('#options').editable({ + value: [2, 3], + source: [ + {value: 1, text: 'option1'}, + {value: 2, text: 'option2'}, + {value: 3, text: 'option3'} + ] + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Checklist = function (options) { + this.init('checklist', options, Checklist.defaults); + }; + + $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); + + $.extend(Checklist.prototype, { + renderList: function() { + var $label, $div; + + this.$tpl.empty(); + + if(!$.isArray(this.sourceData)) { + return; + } + + for(var i=0; i<this.sourceData.length; i++) { + $label = $('<label>').append($('<input>', { + type: 'checkbox', + value: this.sourceData[i].value + })) + .append($('<span>').text(' '+this.sourceData[i].text)); + + $('<div>').append($label).appendTo(this.$tpl); + } + + this.$input = this.$tpl.find('input[type="checkbox"]'); + this.setClass(); + }, + + value2str: function(value) { + return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : ''; + }, + + //parse separated string + str2value: function(str) { + var reg, value = null; + if(typeof str === 'string' && str.length) { + reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*'); + value = str.split(reg); + } else if($.isArray(str)) { + value = str; + } else { + value = [str]; + } + return value; + }, + + //set checked on required checkboxes + value2input: function(value) { + this.$input.prop('checked', false); + if($.isArray(value) && value.length) { + this.$input.each(function(i, el) { + var $el = $(el); + // cannot use $.inArray as it performs strict comparison + $.each(value, function(j, val){ + /*jslint eqeq: true*/ + if($el.val() == val) { + /*jslint eqeq: false*/ + $el.prop('checked', true); + } + }); + }); + } + }, + + input2value: function() { + var checked = []; + this.$input.filter(':checked').each(function(i, el) { + checked.push($(el).val()); + }); + return checked; + }, + + //collect text of checked boxes + value2htmlFinal: function(value, element) { + var html = [], + checked = $.fn.editableutils.itemsByValue(value, this.sourceData); + + if(checked.length) { + $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); + $(element).html(html.join('<br>')); + } else { + $(element).empty(); + } + }, + + activate: function() { + this.$input.first().focus(); + }, + + autosubmit: function() { + this.$input.on('keydown', function(e){ + if (e.which === 13) { + $(this).closest('form').submit(); + } + }); + } + }); + + Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { + /** + @property tpl + @default <div></div> + **/ + tpl:'<div class="editable-checklist"></div>', + + /** + @property inputclass + @type string + @default null + **/ + inputclass: null, + + /** + Separator of values when reading from `data-value` attribute + + @property separator + @type string + @default ',' + **/ + separator: ',' + }); + + $.fn.editabletypes.checklist = Checklist; + +}(window.jQuery)); + +/** +HTML5 input types. +Following types are supported: + +* password +* email +* url +* tel +* number +* range + +Learn more about html5 inputs: +http://www.w3.org/wiki/HTML5_form_additions +To check browser compatibility please see: +https://developer.mozilla.org/en-US/docs/HTML/Element/Input + +@class html5types +@extends text +@final +@since 1.3.0 +@example +<a href="#" id="email" data-type="email" data-pk="1">admin@example.com</a> +<script> +$(function(){ + $('#email').editable({ + url: '/post', + title: 'Enter email' + }); +}); +</script> +**/ + +/** +@property tpl +@default depends on type +**/ + +/* +Password +*/ +(function ($) { + "use strict"; + + var Password = function (options) { + this.init('password', options, Password.defaults); + }; + $.fn.editableutils.inherit(Password, $.fn.editabletypes.text); + $.extend(Password.prototype, { + //do not display password, show '[hidden]' instead + value2html: function(value, element) { + if(value) { + $(element).text('[hidden]'); + } else { + $(element).empty(); + } + }, + //as password not displayed, should not set value by html + html2value: function(html) { + return null; + } + }); + Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="password">' + }); + $.fn.editabletypes.password = Password; +}(window.jQuery)); + + +/* +Email +*/ +(function ($) { + "use strict"; + + var Email = function (options) { + this.init('email', options, Email.defaults); + }; + $.fn.editableutils.inherit(Email, $.fn.editabletypes.text); + Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="email">' + }); + $.fn.editabletypes.email = Email; +}(window.jQuery)); + + +/* +Url +*/ +(function ($) { + "use strict"; + + var Url = function (options) { + this.init('url', options, Url.defaults); + }; + $.fn.editableutils.inherit(Url, $.fn.editabletypes.text); + Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="url">' + }); + $.fn.editabletypes.url = Url; +}(window.jQuery)); + + +/* +Tel +*/ +(function ($) { + "use strict"; + + var Tel = function (options) { + this.init('tel', options, Tel.defaults); + }; + $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text); + Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="tel">' + }); + $.fn.editabletypes.tel = Tel; +}(window.jQuery)); + + +/* +Number +*/ +(function ($) { + "use strict"; + + var NumberInput = function (options) { + this.init('number', options, NumberInput.defaults); + }; + $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text); + $.extend(NumberInput.prototype, { + render: function () { + NumberInput.superclass.render.call(this); + this.setAttr('min'); + this.setAttr('max'); + this.setAttr('step'); + }, + postrender: function() { + if(this.$clear) { + //increase right ffset for up/down arrows + this.$clear.css({right: 24}); + /* + //can position clear button only here, when form is shown and height can be calculated + var h = this.$input.outerHeight(true) || 20, + delta = (h - this.$clear.height()) / 2; + + //add 12px to offset right for up/down arrows + this.$clear.css({top: delta, right: delta + 16}); + */ + } + } + }); + NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="number">', + inputclass: 'input-mini', + min: null, + max: null, + step: null + }); + $.fn.editabletypes.number = NumberInput; +}(window.jQuery)); + + +/* +Range (inherit from number) +*/ +(function ($) { + "use strict"; + + var Range = function (options) { + this.init('range', options, Range.defaults); + }; + $.fn.editableutils.inherit(Range, $.fn.editabletypes.number); + $.extend(Range.prototype, { + render: function () { + this.$input = this.$tpl.filter('input'); + + this.setClass(); + this.setAttr('min'); + this.setAttr('max'); + this.setAttr('step'); + + this.$input.on('input', function(){ + $(this).siblings('output').text($(this).val()); + }); + }, + activate: function() { + this.$input.focus(); + } + }); + Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, { + tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>', + inputclass: 'input-medium' + }); + $.fn.editabletypes.range = Range; +}(window.jQuery)); +/** +Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2. +Please see [original docs](http://ivaynberg.github.com/select2) for detailed description and options. +You should manually include select2 distributive: + + <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> + <script src="select2/select2.js"></script> + +For make it **Bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css): + + <link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link> + +**Note:** currently `ajax` source for select2 is not supported, as it's not possible to load it in closed select2 state. +The solution is to load source manually and assign statically. + +@class select2 +@extends abstractinput +@since 1.4.1 +@final +@example +<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-original-title="Select country"></a> +<script> +$(function(){ + $('#country').editable({ + source: [ + {id: 'gb', text: 'Great Britain'}, + {id: 'us', text: 'United States'}, + {id: 'ru', text: 'Russia'} + ], + select2: { + multiple: true + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Constructor = function (options) { + this.init('select2', options, Constructor.defaults); + + options.select2 = options.select2 || {}; + + var that = this, + mixin = { //mixin to select2 options + placeholder: options.placeholder + }; + + //detect whether it is multi-valued + this.isMultiple = options.select2.tags || options.select2.multiple; + + //if not `tags` mode, we need define initSelection to set data from source + if(!options.select2.tags) { + if(options.source) { + mixin.data = options.source; + } + + //this function can be defaulted in seletc2. See https://github.com/ivaynberg/select2/issues/710 + mixin.initSelection = function (element, callback) { + //temp: try update results + /* + if(options.select2 && options.select2.ajax) { + console.log('attached'); + var original = $(element).data('select2').postprocessResults; + console.log(original); + $(element).data('select2').postprocessResults = function(data, initial) { + console.log('postprocess'); + // this.element.triggerHandler('loaded', [data]); + original.apply(this, arguments); + } + + // $(element).on('loaded', function(){console.log('loaded');}); + $(element).data('select2').updateResults(true); + } + */ + + var val = that.str2value(element.val()), + data = $.fn.editableutils.itemsByValue(val, mixin.data, 'id'); + + //for single-valued mode should not use array. Take first element instead. + if($.isArray(data) && data.length && !that.isMultiple) { + data = data[0]; + } + + callback(data); + }; + } + + //overriding objects in config (as by default jQuery extend() is not recursive) + this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2); + }; + + $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); + + $.extend(Constructor.prototype, { + render: function() { + this.setClass(); + //apply select2 + this.$input.select2(this.options.select2); + + //when data is loaded via ajax, we need to know when it's done + if('ajax' in this.options.select2) { + /* + console.log('attached'); + var original = this.$input.data('select2').postprocessResults; + this.$input.data('select2').postprocessResults = function(data, initial) { + this.element.triggerHandler('loaded', [data]); + original.apply(this, arguments); + } + */ + } + + + //trigger resize of editableform to re-position container in multi-valued mode + if(this.isMultiple) { + this.$input.on('change', function() { + $(this).closest('form').parent().triggerHandler('resize'); + }); + } + }, + + value2html: function(value, element) { + var text = '', data; + if(this.$input) { //called when submitting form and select2 already exists + data = this.$input.select2('data'); + } else { //on init (autotext) + //here select2 instance not created yet and data may be even not loaded. + //we can check data/tags property of select config and if exist lookup text + if(this.options.select2.tags) { + data = value; + } else if(this.options.select2.data) { + data = $.fn.editableutils.itemsByValue(value, this.options.select2.data, 'id'); + } else { + //if('ajax' in this.options.select2) { + } + } + + if($.isArray(data)) { + //collect selected data and show with separator + text = []; + $.each(data, function(k, v){ + text.push(v && typeof v === 'object' ? v.text : v); + }); + } else if(data) { + text = data.text; + } + + text = $.isArray(text) ? text.join(this.options.viewseparator) : text; + + $(element).text(text); + }, + + html2value: function(html) { + return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null; + }, + + value2input: function(value) { + this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + }, + + input2value: function() { + return this.$input.select2('val'); + }, + + str2value: function(str, separator) { + if(typeof str !== 'string' || !this.isMultiple) { + return str; + } + + separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator; + + var val, i, l; + + if (str === null || str.length < 1) { + return null; + } + val = str.split(separator); + for (i = 0, l = val.length; i < l; i = i + 1) { + val[i] = $.trim(val[i]); + } + + return val; + }, + + autosubmit: function() { + this.$input.on('change', function(e, isInitial){ + if(!isInitial) { + $(this).closest('form').submit(); + } + }); + } + + }); + + Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <input type="hidden"> + **/ + tpl:'<input type="hidden">', + /** + Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2). + + @property select2 + @type object + @default null + **/ + select2: null, + /** + Placeholder attribute of select + + @property placeholder + @type string + @default null + **/ + placeholder: null, + /** + Source data for select. It will be assigned to select2 `data` property and kept here just for convenience. + Please note, that format is different from simple `select` input: use 'id' instead of 'value'. + E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`. + + @property source + @type array + @default null + **/ + source: null, + /** + Separator used to display tags. + + @property viewseparator + @type string + @default ', ' + **/ + viewseparator: ', ' + }); + + $.fn.editabletypes.select2 = Constructor; + +}(window.jQuery)); + +/** +* Combodate - 1.0.3 +* Dropdown date and time picker. +* Converts text input into dropdowns to pick day, month, year, hour, minute and second. +* Uses momentjs as datetime library http://momentjs.com. +* For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang +* +* Author: Vitaliy Potapov +* Project page: http://github.com/vitalets/combodate +* Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. +**/ +(function ($) { + + var Combodate = function (element, options) { + this.$element = $(element); + if(!this.$element.is('input')) { + $.error('Combodate should be applied to INPUT element'); + return; + } + this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data()); + this.init(); + }; + + Combodate.prototype = { + constructor: Combodate, + init: function () { + this.map = { + //key regexp moment.method + day: ['D', 'date'], + month: ['M', 'month'], + year: ['Y', 'year'], + hour: ['[Hh]', 'hours'], + minute: ['m', 'minutes'], + second: ['s', 'seconds'], + ampm: ['[Aa]', ''] + }; + + this.$widget = $('<span class="combodate"></span>').html(this.getTemplate()); + + this.initCombos(); + + //update original input on change + this.$widget.on('change', 'select', $.proxy(function(){ + this.$element.val(this.getValue()); + }, this)); + + this.$widget.find('select').css('width', 'auto'); + + //hide original input and insert widget + this.$element.hide().after(this.$widget); + + //set initial value + this.setValue(this.$element.val() || this.options.value); + }, + + /* + Replace tokens in template with <select> elements + */ + getTemplate: function() { + var tpl = this.options.template; + + //first pass + $.each(this.map, function(k, v) { + v = v[0]; + var r = new RegExp(v+'+'), + token = v.length > 1 ? v.substring(1, 2) : v; + + tpl = tpl.replace(r, '{'+token+'}'); + }); + + //replace spaces with + tpl = tpl.replace(/ /g, ' '); + + //second pass + $.each(this.map, function(k, v) { + v = v[0]; + var token = v.length > 1 ? v.substring(1, 2) : v; + + tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>'); + }); + + return tpl; + }, + + /* + Initialize combos that presents in template + */ + initCombos: function() { + var that = this; + $.each(this.map, function(k, v) { + var $c = that.$widget.find('.'+k), f, items; + if($c.length) { + that['$'+k] = $c; //set properties like this.$day, this.$month etc. + f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); //define method name to fill items, e.g `fillDays` + items = that[f](); + that['$'+k].html(that.renderItems(items)); + } + }); + }, + + /* + Initialize items of combos. Handles `firstItem` option + */ + initItems: function(key) { + var values = [], + relTime; + + if(this.options.firstItem === 'name') { + //need both to support moment ver < 2 and >= 2 + relTime = moment.relativeTime || moment.langData()._relativeTime; + var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key]; + //take last entry (see momentjs lang files structure) + header = header.split(' ').reverse()[0]; + values.push(['', header]); + } else if(this.options.firstItem === 'empty') { + values.push(['', '']); + } + return values; + }, + + /* + render items to string of <option> tags + */ + renderItems: function(items) { + var str = []; + for(var i=0; i<items.length; i++) { + str.push('<option value="'+items[i][0]+'">'+items[i][1]+'</option>'); + } + return str.join("\n"); + }, + + /* + fill day + */ + fillDay: function() { + var items = this.initItems('d'), name, i, + twoDigit = this.options.template.indexOf('DD') !== -1; + + for(i=1; i<=31; i++) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill month + */ + fillMonth: function() { + var items = this.initItems('M'), name, i, + longNames = this.options.template.indexOf('MMMM') !== -1, + shortNames = this.options.template.indexOf('MMM') !== -1, + twoDigit = this.options.template.indexOf('MM') !== -1; + + for(i=0; i<=11; i++) { + if(longNames) { + name = moment().month(i).format('MMMM'); + } else if(shortNames) { + name = moment().month(i).format('MMM'); + } else if(twoDigit) { + name = this.leadZero(i+1); + } else { + name = i+1; + } + items.push([i, name]); + } + return items; + }, + + /* + fill year + */ + fillYear: function() { + var items = [], name, i, + longNames = this.options.template.indexOf('YYYY') !== -1; + + for(i=this.options.maxYear; i>=this.options.minYear; i--) { + name = longNames ? i : (i+'').substring(2); + items[this.options.yearDescending ? 'push' : 'unshift']([i, name]); + } + + items = this.initItems('y').concat(items); + + return items; + }, + + /* + fill hour + */ + fillHour: function() { + var items = this.initItems('h'), name, i, + h12 = this.options.template.indexOf('h') !== -1, + h24 = this.options.template.indexOf('H') !== -1, + twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1, + max = h12 ? 12 : 23; + + for(i=0; i<=max; i++) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill minute + */ + fillMinute: function() { + var items = this.initItems('m'), name, i, + twoDigit = this.options.template.indexOf('mm') !== -1; + + for(i=0; i<=59; i+= this.options.minuteStep) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill second + */ + fillSecond: function() { + var items = this.initItems('s'), name, i, + twoDigit = this.options.template.indexOf('ss') !== -1; + + for(i=0; i<=59; i+= this.options.secondStep) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill ampm + */ + fillAmpm: function() { + var ampmL = this.options.template.indexOf('a') !== -1, + ampmU = this.options.template.indexOf('A') !== -1, + items = [ + ['am', ampmL ? 'am' : 'AM'], + ['pm', ampmL ? 'pm' : 'PM'] + ]; + return items; + }, + + /* + Returns current date value. + If format not specified - `options.format` used. + If format = `null` - Moment object returned. + */ + getValue: function(format) { + var dt, values = {}, + that = this, + notSelected = false; + + //getting selected values + $.each(this.map, function(k, v) { + if(k === 'ampm') { + return; + } + var def = k === 'day' ? 1 : 0; + + values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def; + + if(isNaN(values[k])) { + notSelected = true; + return false; + } + }); + + //if at least one visible combo not selected - return empty string + if(notSelected) { + return ''; + } + + //convert hours if 12h format + if(this.$ampm) { + values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; + if(values.hour === 24) { + values.hour = 0; + } + } + + dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]); + + //highlight invalid date + this.highlight(dt); + + format = format === undefined ? this.options.format : format; + if(format === null) { + return dt.isValid() ? dt : null; + } else { + return dt.isValid() ? dt.format(format) : ''; + } + }, + + setValue: function(value) { + if(!value) { + return; + } + + var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value), + that = this, + values = {}; + + //function to find nearest value in select options + function getNearest($select, value) { + var delta = {}; + $select.children('option').each(function(i, opt){ + var optValue = $(opt).attr('value'), + distance; + + if(optValue === '') return; + distance = Math.abs(optValue - value); + if(typeof delta.distance === 'undefined' || distance < delta.distance) { + delta = {value: optValue, distance: distance}; + } + }); + return delta.value; + } + + if(dt.isValid()) { + //read values from date object + $.each(this.map, function(k, v) { + if(k === 'ampm') { + return; + } + values[k] = dt[v[1]](); + }); + + if(this.$ampm) { + if(values.hour > 12) { + values.hour -= 12; + values.ampm = 'pm'; + } else { + values.ampm = 'am'; + } + } + + $.each(values, function(k, v) { + //call val() for each existing combo, e.g. this.$hour.val() + if(that['$'+k]) { + + if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } + + if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } + + that['$'+k].val(v); + } + }); + + this.$element.val(dt.format(this.options.format)); + } + }, + + /* + highlight combos if date is invalid + */ + highlight: function(dt) { + if(!dt.isValid()) { + if(this.options.errorClass) { + this.$widget.addClass(this.options.errorClass); + } else { + //store original border color + if(!this.borderColor) { + this.borderColor = this.$widget.find('select').css('border-color'); + } + this.$widget.find('select').css('border-color', 'red'); + } + } else { + if(this.options.errorClass) { + this.$widget.removeClass(this.options.errorClass); + } else { + this.$widget.find('select').css('border-color', this.borderColor); + } + } + }, + + leadZero: function(v) { + return v <= 9 ? '0' + v : v; + }, + + destroy: function() { + this.$widget.remove(); + this.$element.removeData('combodate').show(); + } + + //todo: clear method + }; + + $.fn.combodate = function ( option ) { + var d, args = Array.apply(null, arguments); + args.shift(); + + //getValue returns date as string / object (not jQuery object) + if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) { + return d.getValue.apply(d, args); + } + + return this.each(function () { + var $this = $(this), + data = $this.data('combodate'), + options = typeof option == 'object' && option; + if (!data) { + $this.data('combodate', (data = new Combodate(this, options))); + } + if (typeof option == 'string' && typeof data[option] == 'function') { + data[option].apply(data, args); + } + }); + }; + + $.fn.combodate.defaults = { + //in this format value stored in original input + format: 'DD-MM-YYYY HH:mm', + //in this format items in dropdowns are displayed + template: 'D / MMM / YYYY H : mm', + //initial value, can be `new Date()` + value: null, + minYear: 1970, + maxYear: 2015, + yearDescending: true, + minuteStep: 5, + secondStep: 1, + firstItem: 'empty', //'name', 'empty', 'none' + errorClass: null, + roundTime: true //whether to round minutes and seconds if step > 1 + }; + +}(window.jQuery)); +/** +Combodate input - dropdown date and time picker. +Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com). + + <script src="js/moment.min.js"></script> + +Allows to input: + +* only date +* only time +* both date and time + +Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker. +Internally value stored as `momentjs` object. + +@class combodate +@extends abstractinput +@final +@since 1.4.0 +@example +<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-original-title="Select date"></a> +<script> +$(function(){ + $('#dob').editable({ + format: 'YYYY-MM-DD', + viewformat: 'DD.MM.YYYY', + template: 'D / MMMM / YYYY', + combodate: { + minYear: 2000, + maxYear: 2015, + minuteStep: 1 + } + } + }); +}); +</script> +**/ + +/*global moment*/ + +(function ($) { + "use strict"; + + var Constructor = function (options) { + this.init('combodate', options, Constructor.defaults); + + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } + + //try parse combodate config defined as json string in data-combodate + options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true); + + //overriding combodate config (as by default jQuery extend() is not recursive) + this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, { + format: this.options.format, + template: this.options.template + }); + }; + + $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); + + $.extend(Constructor.prototype, { + render: function () { + this.$input.combodate(this.options.combodate); + + //"clear" link + /* + if(this.options.clear) { + this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ + e.preventDefault(); + e.stopPropagation(); + this.clear(); + }, this)); + + this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); + } + */ + }, + + value2html: function(value, element) { + var text = value ? value.format(this.options.viewformat) : ''; + $(element).text(text); + }, + + html2value: function(html) { + return html ? moment(html, this.options.viewformat) : null; + }, + + value2str: function(value) { + return value ? value.format(this.options.format) : ''; + }, + + str2value: function(str) { + return str ? moment(str, this.options.format) : null; + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + this.$input.combodate('setValue', value); + }, + + input2value: function() { + return this.$input.combodate('getValue', null); + }, + + activate: function() { + this.$input.siblings('.combodate').find('select').eq(0).focus(); + }, + + /* + clear: function() { + this.$input.data('datepicker').date = null; + this.$input.find('.active').removeClass('active'); + }, + */ + + autosubmit: function() { + + } + + }); + + Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <input type="text"> + **/ + tpl:'<input type="text">', + /** + @property inputclass + @default null + **/ + inputclass: null, + /** + Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br> + See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format) + + @property format + @type string + @default YYYY-MM-DD + **/ + format:'YYYY-MM-DD', + /** + Format used for displaying date. Also applied when converting date from element's text on init. + If not specified equals to `format`. + + @property viewformat + @type string + @default null + **/ + viewformat: null, + /** + Template used for displaying dropdowns. + + @property template + @type string + @default D / MMM / YYYY + **/ + template: 'D / MMM / YYYY', + /** + Configuration of combodate. + Full list of options: http://vitalets.github.com/combodate/#docs + + @property combodate + @type object + @default null + **/ + combodate: null + + /* + (not implemented yet) + Text shown as clear date button. + If <code>false</code> clear button will not be rendered. + + @property clear + @type boolean|string + @default 'x clear' + */ + //clear: '× clear' + }); + + $.fn.editabletypes.combodate = Constructor; + +}(window.jQuery)); + +/* +Editableform based on Twitter Bootstrap +*/ +(function ($) { + "use strict"; + + $.extend($.fn.editableform.Constructor.prototype, { + initTemplate: function() { + this.$form = $($.fn.editableform.template); + this.$form.find('.editable-error-block').addClass('help-block'); + } + }); + + //buttons + $.fn.editableform.buttons = '<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button>'+ + '<button type="button" class="btn editable-cancel"><i class="icon-remove"></i></button>'; + + //error classes + $.fn.editableform.errorGroupClass = 'error'; + $.fn.editableform.errorBlockClass = null; + +}(window.jQuery)); +/** +* Editable Popover +* --------------------- +* requires bootstrap-popover.js +*/ +(function ($) { + "use strict"; + + //extend methods + $.extend($.fn.editableContainer.Popup.prototype, { + containerName: 'popover', + //for compatibility with bootstrap <= 2.2.1 (content inserted into <p> instead of directly .popover-content) + innerCss: $.fn.popover && $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content', + + initContainer: function(){ + $.extend(this.containerOptions, { + trigger: 'manual', + selector: false, + content: ' ', + template: $.fn.popover.defaults.template + }); + + //as template property is used in inputs, hide it from popover + var t; + if(this.$element.data('template')) { + t = this.$element.data('template'); + this.$element.removeData('template'); + } + + this.call(this.containerOptions); + + if(t) { + //restore data('template') + this.$element.data('template', t); + } + }, + + /* show */ + innerShow: function () { + this.call('show'); + }, + + /* hide */ + innerHide: function () { + this.call('hide'); + }, + + /* destroy */ + innerDestroy: function() { + this.call('destroy'); + }, + + setContainerOption: function(key, value) { + this.container().options[key] = value; + }, + + /** + * move popover to new position. This function mainly copied from bootstrap-popover. + */ + /*jshint laxcomma: true*/ + setPosition: function () { + + (function() { + var $tip = this.tip() + , inside + , pos + , actualWidth + , actualHeight + , placement + , tp; + + placement = typeof this.options.placement === 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement; + + inside = /in/.test(placement); + + $tip + // .detach() + //vitalets: remove any placement class because otherwise they dont influence on re-positioning of visible popover + .removeClass('top right bottom left') + .css({ top: 0, left: 0, display: 'block' }); + // .insertAfter(this.$element); + + pos = this.getPosition(inside); + + actualWidth = $tip[0].offsetWidth; + actualHeight = $tip[0].offsetHeight; + + switch (inside ? placement.split(' ')[1] : placement) { + case 'bottom': + tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}; + break; + case 'top': + tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}; + break; + case 'left': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}; + break; + case 'right': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}; + break; + } + + $tip + .offset(tp) + .addClass(placement) + .addClass('in'); + + }).call(this.container()); + /*jshint laxcomma: false*/ + } + }); + +}(window.jQuery)); +/** +Bootstrap-datepicker. +Description and examples: https://github.com/eternicode/bootstrap-datepicker. +For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales +and set `language` option. +Since 1.4.0 date has different appearance in **popup** and **inline** modes. + +@class date +@extends abstractinput +@final +@example +<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a> +<script> +$(function(){ + $('#dob').editable({ + format: 'yyyy-mm-dd', + viewformat: 'dd/mm/yyyy', + datepicker: { + weekStart: 1 + } + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Date = function (options) { + this.init('date', options, Date.defaults); + this.initPicker(options, Date.defaults); + }; + + $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput); + + $.extend(Date.prototype, { + initPicker: function(options, defaults) { + //'format' is set directly from settings or data-* attributes + + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } + + //overriding datepicker config (as by default jQuery extend() is not recursive) + //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only + this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, { + format: this.options.viewformat + }); + + //language + this.options.datepicker.language = this.options.datepicker.language || 'en'; + + //store DPglobal + this.dpg = $.fn.datepicker.DPGlobal; + + //store parsed formats + this.parsedFormat = this.dpg.parseFormat(this.options.format); + this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat); + }, + + render: function () { + this.$input.datepicker(this.options.datepicker); + + //"clear" link + if(this.options.clear) { + this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ + e.preventDefault(); + e.stopPropagation(); + this.clear(); + }, this)); + + this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); + } + }, + + value2html: function(value, element) { + var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''; + Date.superclass.value2html(text, element); + }, + + html2value: function(html) { + return html ? this.dpg.parseDate(html, this.parsedViewFormat, this.options.datepicker.language) : null; + }, + + value2str: function(value) { + return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : ''; + }, + + str2value: function(str) { + return str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datepicker.language) : null; + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + this.$input.datepicker('update', value); + }, + + input2value: function() { + return this.$input.data('datepicker').date; + }, + + activate: function() { + }, + + clear: function() { + this.$input.data('datepicker').date = null; + this.$input.find('.active').removeClass('active'); + if(!this.options.showbuttons) { + this.$input.closest('form').submit(); + } + }, + + autosubmit: function() { + this.$input.on('mouseup', '.day', function(e){ + if($(e.currentTarget).is('.old') || $(e.currentTarget).is('.new')) { + return; + } + var $form = $(this).closest('form'); + setTimeout(function() { + $form.submit(); + }, 200); + }); + //changedate is not suitable as it triggered when showing datepicker. see #149 + /* + this.$input.on('changeDate', function(e){ + var $form = $(this).closest('form'); + setTimeout(function() { + $form.submit(); + }, 200); + }); + */ + } + + }); + + Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <div></div> + **/ + tpl:'<div class="editable-date well"></div>', + /** + @property inputclass + @default null + **/ + inputclass: null, + /** + Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br> + Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code> + + @property format + @type string + @default yyyy-mm-dd + **/ + format:'yyyy-mm-dd', + /** + Format used for displaying date. Also applied when converting date from element's text on init. + If not specified equals to <code>format</code> + + @property viewformat + @type string + @default null + **/ + viewformat: null, + /** + Configuration of datepicker. + Full list of options: http://vitalets.github.com/bootstrap-datepicker + + @property datepicker + @type object + @default { + weekStart: 0, + startView: 0, + minViewMode: 0, + autoclose: false + } + **/ + datepicker:{ + weekStart: 0, + startView: 0, + minViewMode: 0, + autoclose: false + }, + /** + Text shown as clear date button. + If <code>false</code> clear button will not be rendered. + + @property clear + @type boolean|string + @default 'x clear' + **/ + clear: '× clear' + }); + + $.fn.editabletypes.date = Date; + +}(window.jQuery)); + +/** +Bootstrap datefield input - modification for inline mode. +Shows normal <input type="text"> and binds popup datepicker. +Automatically shown in inline mode. + +@class datefield +@extends date + +@since 1.4.0 +**/ +(function ($) { + "use strict"; + + var DateField = function (options) { + this.init('datefield', options, DateField.defaults); + this.initPicker(options, DateField.defaults); + }; + + $.fn.editableutils.inherit(DateField, $.fn.editabletypes.date); + + $.extend(DateField.prototype, { + render: function () { + this.$input = this.$tpl.find('input'); + this.setClass(); + this.setAttr('placeholder'); + + this.$tpl.datepicker(this.options.datepicker); + + //need to disable original event handlers + this.$input.off('focus keydown'); + + //update value of datepicker + this.$input.keyup($.proxy(function(){ + this.$tpl.removeData('date'); + this.$tpl.datepicker('update'); + }, this)); + + }, + + value2input: function(value) { + this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''); + this.$tpl.datepicker('update'); + }, + + input2value: function() { + return this.html2value(this.$input.val()); + }, + + activate: function() { + $.fn.editabletypes.text.prototype.activate.call(this); + }, + + autosubmit: function() { + //reset autosubmit to empty + } + }); + + DateField.defaults = $.extend({}, $.fn.editabletypes.date.defaults, { + /** + @property tpl + **/ + tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>', + /** + @property inputclass + @default 'input-small' + **/ + inputclass: 'input-small', + + /* datepicker config */ + datepicker: { + weekStart: 0, + startView: 0, + minViewMode: 0, + autoclose: true + } + }); + + $.fn.editabletypes.datefield = DateField; + +}(window.jQuery)); +/* ========================================================= + * bootstrap-datepicker.js + * http://www.eyecon.ro/bootstrap-datepicker + * ========================================================= + * Copyright 2012 Stefan Petre + * Improvements by Andrew Rowls + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + +!function( $ ) { + + function UTCDate(){ + return new Date(Date.UTC.apply(Date, arguments)); + } + function UTCToday(){ + var today = new Date(); + return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()); + } + + // Picker object + + var Datepicker = function(element, options) { + var that = this; + + this.element = $(element); + this.language = options.language||this.element.data('date-language')||"en"; + this.language = this.language in dates ? this.language : this.language.split('-')[0]; //Check if "de-DE" style date is available, if not language should fallback to 2 letter code eg "de" + this.language = this.language in dates ? this.language : "en"; + this.isRTL = dates[this.language].rtl||false; + this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||dates[this.language].format||'mm/dd/yyyy'); + this.isInline = false; + this.isInput = this.element.is('input'); + this.component = this.element.is('.date') ? this.element.find('.add-on, .btn') : false; + this.hasInput = this.component && this.element.find('input').length; + if(this.component && this.component.length === 0) + this.component = false; + + this.forceParse = true; + if ('forceParse' in options) { + this.forceParse = options.forceParse; + } else if ('dateForceParse' in this.element.data()) { + this.forceParse = this.element.data('date-force-parse'); + } + + this.picker = $(DPGlobal.template); + this._buildEvents(); + this._attachEvents(); + + if(this.isInline) { + this.picker.addClass('datepicker-inline').appendTo(this.element); + } else { + this.picker.addClass('datepicker-dropdown dropdown-menu'); + } + if (this.isRTL){ + this.picker.addClass('datepicker-rtl'); + this.picker.find('.prev i, .next i') + .toggleClass('icon-arrow-left icon-arrow-right'); + } + + this.autoclose = false; + if ('autoclose' in options) { + this.autoclose = options.autoclose; + } else if ('dateAutoclose' in this.element.data()) { + this.autoclose = this.element.data('date-autoclose'); + } + + this.keyboardNavigation = true; + if ('keyboardNavigation' in options) { + this.keyboardNavigation = options.keyboardNavigation; + } else if ('dateKeyboardNavigation' in this.element.data()) { + this.keyboardNavigation = this.element.data('date-keyboard-navigation'); + } + + this.viewMode = this.startViewMode = 0; + switch(options.startView || this.element.data('date-start-view')){ + case 2: + case 'decade': + this.viewMode = this.startViewMode = 2; + break; + case 1: + case 'year': + this.viewMode = this.startViewMode = 1; + break; + } + + this.minViewMode = options.minViewMode||this.element.data('date-min-view-mode')||0; + if (typeof this.minViewMode === 'string') { + switch (this.minViewMode) { + case 'months': + this.minViewMode = 1; + break; + case 'years': + this.minViewMode = 2; + break; + default: + this.minViewMode = 0; + break; + } + } + + this.viewMode = this.startViewMode = Math.max(this.startViewMode, this.minViewMode); + + this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false); + this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false); + + this.calendarWeeks = false; + if ('calendarWeeks' in options) { + this.calendarWeeks = options.calendarWeeks; + } else if ('dateCalendarWeeks' in this.element.data()) { + this.calendarWeeks = this.element.data('date-calendar-weeks'); + } + if (this.calendarWeeks) + this.picker.find('tfoot th.today') + .attr('colspan', function(i, val){ + return parseInt(val) + 1; + }); + + this._allow_update = false; + + this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7); + this.weekEnd = ((this.weekStart + 6) % 7); + this.startDate = -Infinity; + this.endDate = Infinity; + this.daysOfWeekDisabled = []; + this.setStartDate(options.startDate||this.element.data('date-startdate')); + this.setEndDate(options.endDate||this.element.data('date-enddate')); + this.setDaysOfWeekDisabled(options.daysOfWeekDisabled||this.element.data('date-days-of-week-disabled')); + this.fillDow(); + this.fillMonths(); + + this._allow_update = true; + + this.update(); + this.showMode(); + + if(this.isInline) { + this.show(); + } + }; + + Datepicker.prototype = { + constructor: Datepicker, + + _events: [], + _secondaryEvents: [], + _applyEvents: function(evs){ + for (var i=0, el, ev; i<evs.length; i++){ + el = evs[i][0]; + ev = evs[i][1]; + el.on(ev); + } + }, + _unapplyEvents: function(evs){ + for (var i=0, el, ev; i<evs.length; i++){ + el = evs[i][0]; + ev = evs[i][1]; + el.off(ev); + } + }, + _buildEvents: function(){ + if (this.isInput) { // single input + this._events = [ + [this.element, { + focus: $.proxy(this.show, this), + keyup: $.proxy(this.update, this), + keydown: $.proxy(this.keydown, this) + }] + ]; + } + else if (this.component && this.hasInput){ // component: input + button + this._events = [ + // For components that are not readonly, allow keyboard nav + [this.element.find('input'), { + focus: $.proxy(this.show, this), + keyup: $.proxy(this.update, this), + keydown: $.proxy(this.keydown, this) + }], + [this.component, { + click: $.proxy(this.show, this) + }] + ]; + } + else if (this.element.is('div')) { // inline datepicker + this.isInline = true; + } + else { + this._events = [ + [this.element, { + click: $.proxy(this.show, this) + }] + ]; + } + + this._secondaryEvents = [ + [this.picker, { + click: $.proxy(this.click, this) + }], + [$(window), { + resize: $.proxy(this.place, this) + }], + [$(document), { + mousedown: $.proxy(function (e) { + // Clicked outside the datepicker, hide it + if ($(e.target).closest('.datepicker.datepicker-inline, .datepicker.datepicker-dropdown').length === 0) { + this.hide(); + } + }, this) + }] + ]; + }, + _attachEvents: function(){ + this._detachEvents(); + this._applyEvents(this._events); + }, + _detachEvents: function(){ + this._unapplyEvents(this._events); + }, + _attachSecondaryEvents: function(){ + this._detachSecondaryEvents(); + this._applyEvents(this._secondaryEvents); + }, + _detachSecondaryEvents: function(){ + this._unapplyEvents(this._secondaryEvents); + }, + + show: function(e) { + if (!this.isInline) + this.picker.appendTo('body'); + this.picker.show(); + this.height = this.component ? this.component.outerHeight() : this.element.outerHeight(); + this.place(); + this._attachSecondaryEvents(); + if (e) { + e.preventDefault(); + } + this.element.trigger({ + type: 'show', + date: this.date + }); + }, + + hide: function(e){ + if(this.isInline) return; + if (!this.picker.is(':visible')) return; + this.picker.hide().detach(); + this._detachSecondaryEvents(); + this.viewMode = this.startViewMode; + this.showMode(); + + if ( + this.forceParse && + ( + this.isInput && this.element.val() || + this.hasInput && this.element.find('input').val() + ) + ) + this.setValue(); + this.element.trigger({ + type: 'hide', + date: this.date + }); + }, + + remove: function() { + this.hide(); + this._detachEvents(); + this._detachSecondaryEvents(); + this.picker.remove(); + delete this.element.data().datepicker; + if (!this.isInput) { + delete this.element.data().date; + } + }, + + getDate: function() { + var d = this.getUTCDate(); + return new Date(d.getTime() + (d.getTimezoneOffset()*60000)); + }, + + getUTCDate: function() { + return this.date; + }, + + setDate: function(d) { + this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000))); + }, + + setUTCDate: function(d) { + this.date = d; + this.setValue(); + }, + + setValue: function() { + var formatted = this.getFormattedDate(); + if (!this.isInput) { + if (this.component){ + this.element.find('input').val(formatted); + } + this.element.data('date', formatted); + } else { + this.element.val(formatted); + } + }, + + getFormattedDate: function(format) { + if (format === undefined) + format = this.format; + return DPGlobal.formatDate(this.date, format, this.language); + }, + + setStartDate: function(startDate){ + this.startDate = startDate||-Infinity; + if (this.startDate !== -Infinity) { + this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language); + } + this.update(); + this.updateNavArrows(); + }, + + setEndDate: function(endDate){ + this.endDate = endDate||Infinity; + if (this.endDate !== Infinity) { + this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language); + } + this.update(); + this.updateNavArrows(); + }, + + setDaysOfWeekDisabled: function(daysOfWeekDisabled){ + this.daysOfWeekDisabled = daysOfWeekDisabled||[]; + if (!$.isArray(this.daysOfWeekDisabled)) { + this.daysOfWeekDisabled = this.daysOfWeekDisabled.split(/,\s*/); + } + this.daysOfWeekDisabled = $.map(this.daysOfWeekDisabled, function (d) { + return parseInt(d, 10); + }); + this.update(); + this.updateNavArrows(); + }, + + place: function(){ + if(this.isInline) return; + var zIndex = parseInt(this.element.parents().filter(function() { + return $(this).css('z-index') != 'auto'; + }).first().css('z-index'))+10; + var offset = this.component ? this.component.parent().offset() : this.element.offset(); + var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(true); + this.picker.css({ + top: offset.top + height, + left: offset.left, + zIndex: zIndex + }); + }, + + _allow_update: true, + update: function(){ + if (!this._allow_update) return; + + var date, fromArgs = false; + if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) { + date = arguments[0]; + fromArgs = true; + } else { + date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val(); + } + + this.date = DPGlobal.parseDate(date, this.format, this.language); + + if(fromArgs) this.setValue(); + + if (this.date < this.startDate) { + this.viewDate = new Date(this.startDate); + } else if (this.date > this.endDate) { + this.viewDate = new Date(this.endDate); + } else { + this.viewDate = new Date(this.date); + } + this.fill(); + }, + + fillDow: function(){ + var dowCnt = this.weekStart, + html = '<tr>'; + if(this.calendarWeeks){ + var cell = '<th class="cw"> </th>'; + html += cell; + this.picker.find('.datepicker-days thead tr:first-child').prepend(cell); + } + while (dowCnt < this.weekStart + 7) { + html += '<th class="dow">'+dates[this.language].daysMin[(dowCnt++)%7]+'</th>'; + } + html += '</tr>'; + this.picker.find('.datepicker-days thead').append(html); + }, + + fillMonths: function(){ + var html = '', + i = 0; + while (i < 12) { + html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>'; + } + this.picker.find('.datepicker-months td').html(html); + }, + + fill: function() { + var d = new Date(this.viewDate), + year = d.getUTCFullYear(), + month = d.getUTCMonth(), + startYear = this.startDate !== -Infinity ? this.startDate.getUTCFullYear() : -Infinity, + startMonth = this.startDate !== -Infinity ? this.startDate.getUTCMonth() : -Infinity, + endYear = this.endDate !== Infinity ? this.endDate.getUTCFullYear() : Infinity, + endMonth = this.endDate !== Infinity ? this.endDate.getUTCMonth() : Infinity, + currentDate = this.date && this.date.valueOf(), + today = new Date(); + this.picker.find('.datepicker-days thead th.switch') + .text(dates[this.language].months[month]+' '+year); + this.picker.find('tfoot th.today') + .text(dates[this.language].today) + .toggle(this.todayBtn !== false); + this.updateNavArrows(); + this.fillMonths(); + var prevMonth = UTCDate(year, month-1, 28,0,0,0,0), + day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth()); + prevMonth.setUTCDate(day); + prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7)%7); + var nextMonth = new Date(prevMonth); + nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); + nextMonth = nextMonth.valueOf(); + var html = []; + var clsName; + while(prevMonth.valueOf() < nextMonth) { + if (prevMonth.getUTCDay() == this.weekStart) { + html.push('<tr>'); + if(this.calendarWeeks){ + // ISO 8601: First week contains first thursday. + // ISO also states week starts on Monday, but we can be more abstract here. + var + // Start of current week: based on weekstart/current date + ws = new Date(+prevMonth + (this.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5), + // Thursday of this week + th = new Date(+ws + (7 + 4 - ws.getUTCDay()) % 7 * 864e5), + // First Thursday of year, year from thursday + yth = new Date(+(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5), + // Calendar week: ms between thursdays, div ms per day, div 7 days + calWeek = (th - yth) / 864e5 / 7 + 1; + html.push('<td class="cw">'+ calWeek +'</td>'); + + } + } + clsName = ''; + if (prevMonth.getUTCFullYear() < year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() < month)) { + clsName += ' old'; + } else if (prevMonth.getUTCFullYear() > year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() > month)) { + clsName += ' new'; + } + // Compare internal UTC date with local today, not UTC today + if (this.todayHighlight && + prevMonth.getUTCFullYear() == today.getFullYear() && + prevMonth.getUTCMonth() == today.getMonth() && + prevMonth.getUTCDate() == today.getDate()) { + clsName += ' today'; + } + if (currentDate && prevMonth.valueOf() == currentDate) { + clsName += ' active'; + } + if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate || + $.inArray(prevMonth.getUTCDay(), this.daysOfWeekDisabled) !== -1) { + clsName += ' disabled'; + } + html.push('<td class="day'+clsName+'">'+prevMonth.getUTCDate() + '</td>'); + if (prevMonth.getUTCDay() == this.weekEnd) { + html.push('</tr>'); + } + prevMonth.setUTCDate(prevMonth.getUTCDate()+1); + } + this.picker.find('.datepicker-days tbody').empty().append(html.join('')); + var currentYear = this.date && this.date.getUTCFullYear(); + + var months = this.picker.find('.datepicker-months') + .find('th:eq(1)') + .text(year) + .end() + .find('span').removeClass('active'); + if (currentYear && currentYear == year) { + months.eq(this.date.getUTCMonth()).addClass('active'); + } + if (year < startYear || year > endYear) { + months.addClass('disabled'); + } + if (year == startYear) { + months.slice(0, startMonth).addClass('disabled'); + } + if (year == endYear) { + months.slice(endMonth+1).addClass('disabled'); + } + + html = ''; + year = parseInt(year/10, 10) * 10; + var yearCont = this.picker.find('.datepicker-years') + .find('th:eq(1)') + .text(year + '-' + (year + 9)) + .end() + .find('td'); + year -= 1; + for (var i = -1; i < 11; i++) { + html += '<span class="year'+(i == -1 || i == 10 ? ' old' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>'; + year += 1; + } + yearCont.html(html); + }, + + updateNavArrows: function() { + if (!this._allow_update) return; + + var d = new Date(this.viewDate), + year = d.getUTCFullYear(), + month = d.getUTCMonth(); + switch (this.viewMode) { + case 0: + if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) { + this.picker.find('.prev').css({visibility: 'hidden'}); + } else { + this.picker.find('.prev').css({visibility: 'visible'}); + } + if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() && month >= this.endDate.getUTCMonth()) { + this.picker.find('.next').css({visibility: 'hidden'}); + } else { + this.picker.find('.next').css({visibility: 'visible'}); + } + break; + case 1: + case 2: + if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) { + this.picker.find('.prev').css({visibility: 'hidden'}); + } else { + this.picker.find('.prev').css({visibility: 'visible'}); + } + if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear()) { + this.picker.find('.next').css({visibility: 'hidden'}); + } else { + this.picker.find('.next').css({visibility: 'visible'}); + } + break; + } + }, + + click: function(e) { + e.preventDefault(); + var target = $(e.target).closest('span, td, th'); + if (target.length == 1) { + switch(target[0].nodeName.toLowerCase()) { + case 'th': + switch(target[0].className) { + case 'switch': + this.showMode(1); + break; + case 'prev': + case 'next': + var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1); + switch(this.viewMode){ + case 0: + this.viewDate = this.moveMonth(this.viewDate, dir); + break; + case 1: + case 2: + this.viewDate = this.moveYear(this.viewDate, dir); + break; + } + this.fill(); + break; + case 'today': + var date = new Date(); + date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); + + this.showMode(-2); + var which = this.todayBtn == 'linked' ? null : 'view'; + this._setDate(date, which); + break; + } + break; + case 'span': + if (!target.is('.disabled')) { + this.viewDate.setUTCDate(1); + if (target.is('.month')) { + var day = 1; + var month = target.parent().find('span').index(target); + var year = this.viewDate.getUTCFullYear(); + this.viewDate.setUTCMonth(month); + this.element.trigger({ + type: 'changeMonth', + date: this.viewDate + }); + if ( this.minViewMode == 1 ) { + this._setDate(UTCDate(year, month, day,0,0,0,0)); + } + } else { + var year = parseInt(target.text(), 10)||0; + var day = 1; + var month = 0; + this.viewDate.setUTCFullYear(year); + this.element.trigger({ + type: 'changeYear', + date: this.viewDate + }); + if ( this.minViewMode == 2 ) { + this._setDate(UTCDate(year, month, day,0,0,0,0)); + } + } + this.showMode(-1); + this.fill(); + } + break; + case 'td': + if (target.is('.day') && !target.is('.disabled')){ + var day = parseInt(target.text(), 10)||1; + var year = this.viewDate.getUTCFullYear(), + month = this.viewDate.getUTCMonth(); + if (target.is('.old')) { + if (month === 0) { + month = 11; + year -= 1; + } else { + month -= 1; + } + } else if (target.is('.new')) { + if (month == 11) { + month = 0; + year += 1; + } else { + month += 1; + } + } + this._setDate(UTCDate(year, month, day,0,0,0,0)); + } + break; + } + } + }, + + _setDate: function(date, which){ + if (!which || which == 'date') + this.date = date; + if (!which || which == 'view') + this.viewDate = date; + this.fill(); + this.setValue(); + this.element.trigger({ + type: 'changeDate', + date: this.date + }); + var element; + if (this.isInput) { + element = this.element; + } else if (this.component){ + element = this.element.find('input'); + } + if (element) { + element.change(); + if (this.autoclose && (!which || which == 'date')) { + this.hide(); + } + } + }, + + moveMonth: function(date, dir){ + if (!dir) return date; + var new_date = new Date(date.valueOf()), + day = new_date.getUTCDate(), + month = new_date.getUTCMonth(), + mag = Math.abs(dir), + new_month, test; + dir = dir > 0 ? 1 : -1; + if (mag == 1){ + test = dir == -1 + // If going back one month, make sure month is not current month + // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02) + ? function(){ return new_date.getUTCMonth() == month; } + // If going forward one month, make sure month is as expected + // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02) + : function(){ return new_date.getUTCMonth() != new_month; }; + new_month = month + dir; + new_date.setUTCMonth(new_month); + // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11 + if (new_month < 0 || new_month > 11) + new_month = (new_month + 12) % 12; + } else { + // For magnitudes >1, move one month at a time... + for (var i=0; i<mag; i++) + // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)... + new_date = this.moveMonth(new_date, dir); + // ...then reset the day, keeping it in the new month + new_month = new_date.getUTCMonth(); + new_date.setUTCDate(day); + test = function(){ return new_month != new_date.getUTCMonth(); }; + } + // Common date-resetting loop -- if date is beyond end of month, make it + // end of month + while (test()){ + new_date.setUTCDate(--day); + new_date.setUTCMonth(new_month); + } + return new_date; + }, + + moveYear: function(date, dir){ + return this.moveMonth(date, dir*12); + }, + + dateWithinRange: function(date){ + return date >= this.startDate && date <= this.endDate; + }, + + keydown: function(e){ + if (this.picker.is(':not(:visible)')){ + if (e.keyCode == 27) // allow escape to hide and re-show picker + this.show(); + return; + } + var dateChanged = false, + dir, day, month, + newDate, newViewDate; + switch(e.keyCode){ + case 27: // escape + this.hide(); + e.preventDefault(); + break; + case 37: // left + case 39: // right + if (!this.keyboardNavigation) break; + dir = e.keyCode == 37 ? -1 : 1; + if (e.ctrlKey){ + newDate = this.moveYear(this.date, dir); + newViewDate = this.moveYear(this.viewDate, dir); + } else if (e.shiftKey){ + newDate = this.moveMonth(this.date, dir); + newViewDate = this.moveMonth(this.viewDate, dir); + } else { + newDate = new Date(this.date); + newDate.setUTCDate(this.date.getUTCDate() + dir); + newViewDate = new Date(this.viewDate); + newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir); + } + if (this.dateWithinRange(newDate)){ + this.date = newDate; + this.viewDate = newViewDate; + this.setValue(); + this.update(); + e.preventDefault(); + dateChanged = true; + } + break; + case 38: // up + case 40: // down + if (!this.keyboardNavigation) break; + dir = e.keyCode == 38 ? -1 : 1; + if (e.ctrlKey){ + newDate = this.moveYear(this.date, dir); + newViewDate = this.moveYear(this.viewDate, dir); + } else if (e.shiftKey){ + newDate = this.moveMonth(this.date, dir); + newViewDate = this.moveMonth(this.viewDate, dir); + } else { + newDate = new Date(this.date); + newDate.setUTCDate(this.date.getUTCDate() + dir * 7); + newViewDate = new Date(this.viewDate); + newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7); + } + if (this.dateWithinRange(newDate)){ + this.date = newDate; + this.viewDate = newViewDate; + this.setValue(); + this.update(); + e.preventDefault(); + dateChanged = true; + } + break; + case 13: // enter + this.hide(); + e.preventDefault(); + break; + case 9: // tab + this.hide(); + break; + } + if (dateChanged){ + this.element.trigger({ + type: 'changeDate', + date: this.date + }); + var element; + if (this.isInput) { + element = this.element; + } else if (this.component){ + element = this.element.find('input'); + } + if (element) { + element.change(); + } + } + }, + + showMode: function(dir) { + if (dir) { + this.viewMode = Math.max(this.minViewMode, Math.min(2, this.viewMode + dir)); + } + /* + vitalets: fixing bug of very special conditions: + jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover. + Method show() does not set display css correctly and datepicker is not shown. + Changed to .css('display', 'block') solve the problem. + See https://github.com/vitalets/x-editable/issues/37 + + In jquery 1.7.2+ everything works fine. + */ + //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); + this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block'); + this.updateNavArrows(); + } + }; + + $.fn.datepicker = function ( option ) { + var args = Array.apply(null, arguments); + args.shift(); + return this.each(function () { + var $this = $(this), + data = $this.data('datepicker'), + options = typeof option == 'object' && option; + if (!data) { + $this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options)))); + } + if (typeof option == 'string' && typeof data[option] == 'function') { + data[option].apply(data, args); + } + }); + }; + + $.fn.datepicker.defaults = { + }; + $.fn.datepicker.Constructor = Datepicker; + var dates = $.fn.datepicker.dates = { + en: { + days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], + daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], + months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + today: "Today" + } + }; + + var DPGlobal = { + modes: [ + { + clsName: 'days', + navFnc: 'Month', + navStep: 1 + }, + { + clsName: 'months', + navFnc: 'FullYear', + navStep: 1 + }, + { + clsName: 'years', + navFnc: 'FullYear', + navStep: 10 + }], + isLeapYear: function (year) { + return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)); + }, + getDaysInMonth: function (year, month) { + return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; + }, + validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g, + nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g, + parseFormat: function(format){ + // IE treats \0 as a string end in inputs (truncating the value), + // so it's a bad format delimiter, anyway + var separators = format.replace(this.validParts, '\0').split('\0'), + parts = format.match(this.validParts); + if (!separators || !separators.length || !parts || parts.length === 0){ + throw new Error("Invalid date format."); + } + return {separators: separators, parts: parts}; + }, + parseDate: function(date, format, language) { + if (date instanceof Date) return date; + if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) { + var part_re = /([\-+]\d+)([dmwy])/, + parts = date.match(/([\-+]\d+)([dmwy])/g), + part, dir; + date = new Date(); + for (var i=0; i<parts.length; i++) { + part = part_re.exec(parts[i]); + dir = parseInt(part[1]); + switch(part[2]){ + case 'd': + date.setUTCDate(date.getUTCDate() + dir); + break; + case 'm': + date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir); + break; + case 'w': + date.setUTCDate(date.getUTCDate() + dir * 7); + break; + case 'y': + date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir); + break; + } + } + return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0); + } + var parts = date && date.match(this.nonpunctuation) || [], + date = new Date(), + parsed = {}, + setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'], + setters_map = { + yyyy: function(d,v){ return d.setUTCFullYear(v); }, + yy: function(d,v){ return d.setUTCFullYear(2000+v); }, + m: function(d,v){ + v -= 1; + while (v<0) v += 12; + v %= 12; + d.setUTCMonth(v); + while (d.getUTCMonth() != v) + d.setUTCDate(d.getUTCDate()-1); + return d; + }, + d: function(d,v){ return d.setUTCDate(v); } + }, + val, filtered, part; + setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m']; + setters_map['dd'] = setters_map['d']; + date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); + var fparts = format.parts.slice(); + // Remove noop parts + if (parts.length != fparts.length) { + fparts = $(fparts).filter(function(i,p){ + return $.inArray(p, setters_order) !== -1; + }).toArray(); + } + // Process remainder + if (parts.length == fparts.length) { + for (var i=0, cnt = fparts.length; i < cnt; i++) { + val = parseInt(parts[i], 10); + part = fparts[i]; + if (isNaN(val)) { + switch(part) { + case 'MM': + filtered = $(dates[language].months).filter(function(){ + var m = this.slice(0, parts[i].length), + p = parts[i].slice(0, m.length); + return m == p; + }); + val = $.inArray(filtered[0], dates[language].months) + 1; + break; + case 'M': + filtered = $(dates[language].monthsShort).filter(function(){ + var m = this.slice(0, parts[i].length), + p = parts[i].slice(0, m.length); + return m == p; + }); + val = $.inArray(filtered[0], dates[language].monthsShort) + 1; + break; + } + } + parsed[part] = val; + } + for (var i=0, s; i<setters_order.length; i++){ + s = setters_order[i]; + if (s in parsed && !isNaN(parsed[s])) + setters_map[s](date, parsed[s]); + } + } + return date; + }, + formatDate: function(date, format, language){ + var val = { + d: date.getUTCDate(), + D: dates[language].daysShort[date.getUTCDay()], + DD: dates[language].days[date.getUTCDay()], + m: date.getUTCMonth() + 1, + M: dates[language].monthsShort[date.getUTCMonth()], + MM: dates[language].months[date.getUTCMonth()], + yy: date.getUTCFullYear().toString().substring(2), + yyyy: date.getUTCFullYear() + }; + val.dd = (val.d < 10 ? '0' : '') + val.d; + val.mm = (val.m < 10 ? '0' : '') + val.m; + var date = [], + seps = $.extend([], format.separators); + for (var i=0, cnt = format.parts.length; i < cnt; i++) { + if (seps.length) + date.push(seps.shift()); + date.push(val[format.parts[i]]); + } + return date.join(''); + }, + headTemplate: '<thead>'+ + '<tr>'+ + '<th class="prev"><i class="icon-arrow-left"/></th>'+ + '<th colspan="5" class="switch"></th>'+ + '<th class="next"><i class="icon-arrow-right"/></th>'+ + '</tr>'+ + '</thead>', + contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>', + footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr></tfoot>' + }; + DPGlobal.template = '<div class="datepicker">'+ + '<div class="datepicker-days">'+ + '<table class=" table-condensed">'+ + DPGlobal.headTemplate+ + '<tbody></tbody>'+ + DPGlobal.footTemplate+ + '</table>'+ + '</div>'+ + '<div class="datepicker-months">'+ + '<table class="table-condensed">'+ + DPGlobal.headTemplate+ + DPGlobal.contTemplate+ + DPGlobal.footTemplate+ + '</table>'+ + '</div>'+ + '<div class="datepicker-years">'+ + '<table class="table-condensed">'+ + DPGlobal.headTemplate+ + DPGlobal.contTemplate+ + DPGlobal.footTemplate+ + '</table>'+ + '</div>'+ + '</div>'; + + $.fn.datepicker.DPGlobal = DPGlobal; + +}( window.jQuery ); + +/** +Bootstrap-datetimepicker. +Based on [smalot bootstrap-datetimepicker plugin](https://github.com/smalot/bootstrap-datetimepicker). +Before usage you should manually include dependent js and css: + + <link href="css/datetimepicker.css" rel="stylesheet" type="text/css"></link> + <script src="js/bootstrap-datetimepicker.js"></script> + +For **i18n** you should include js file from here: https://github.com/smalot/bootstrap-datetimepicker/tree/master/js/locales +and set `language` option. + +@class datetime +@extends abstractinput +@final +@since 1.4.4 +@example +<a href="#" id="last_seen" data-type="datetime" data-pk="1" data-url="/post" title="Select date & time">15/03/2013 12:45</a> +<script> +$(function(){ + $('#last_seen').editable({ + format: 'yyyy-mm-dd hh:ii', + viewformat: 'dd/mm/yyyy hh:ii', + datetimepicker: { + weekStart: 1 + } + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var DateTime = function (options) { + this.init('datetime', options, DateTime.defaults); + this.initPicker(options, DateTime.defaults); + }; + + $.fn.editableutils.inherit(DateTime, $.fn.editabletypes.abstractinput); + + $.extend(DateTime.prototype, { + initPicker: function(options, defaults) { + //'format' is set directly from settings or data-* attributes + + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } + + //overriding datetimepicker config (as by default jQuery extend() is not recursive) + //since 1.4 datetimepicker internally uses viewformat instead of format. Format is for submit only + this.options.datetimepicker = $.extend({}, defaults.datetimepicker, options.datetimepicker, { + format: this.options.viewformat + }); + + //language + this.options.datetimepicker.language = this.options.datetimepicker.language || 'en'; + + //store DPglobal + this.dpg = $.fn.datetimepicker.DPGlobal; + + //store parsed formats + this.parsedFormat = this.dpg.parseFormat(this.options.format, this.options.formatType); + this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat, this.options.formatType); + + // + this.options.datetimepicker.startView = this.options.startView; + this.options.datetimepicker.minView = this.options.minView; + this.options.datetimepicker.maxView = this.options.maxView; + }, + + render: function () { + this.$input.datetimepicker(this.options.datetimepicker); + + //"clear" link + if(this.options.clear) { + this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ + e.preventDefault(); + e.stopPropagation(); + this.clear(); + }, this)); + + this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); + } + }, + + value2html: function(value, element) { + //formatDate works with UTCDate! + var text = value ? this.dpg.formatDate(this.toUTC(value), this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : ''; + if(element) { + DateTime.superclass.value2html(text, element); + } else { + return text; + } + }, + + html2value: function(html) { + //parseDate return utc date! + var value = html ? this.dpg.parseDate(html, this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : null; + return value ? this.fromUTC(value) : null; + }, + + value2str: function(value) { + //formatDate works with UTCDate! + return value ? this.dpg.formatDate(this.toUTC(value), this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : ''; + }, + + str2value: function(str) { + //parseDate return utc date! + var value = str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : null; + return value ? this.fromUTC(value) : null; + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + if(value) { + this.$input.data('datetimepicker').setDate(value); + } + }, + + input2value: function() { + //date may be cleared, in that case getDate() triggers error + var dt = this.$input.data('datetimepicker'); + return dt.date ? dt.getDate() : null; + }, + + activate: function() { + }, + + clear: function() { + this.$input.data('datetimepicker').date = null; + this.$input.find('.active').removeClass('active'); + if(!this.options.showbuttons) { + this.$input.closest('form').submit(); + } + }, + + autosubmit: function() { + this.$input.on('mouseup', '.minute', function(e){ + var $form = $(this).closest('form'); + setTimeout(function() { + $form.submit(); + }, 200); + }); + }, + + //convert date from local to utc + toUTC: function(value) { + return value ? new Date(value.valueOf() - value.getTimezoneOffset() * 60000) : value; + }, + + //convert date from utc to local + fromUTC: function(value) { + return value ? new Date(value.valueOf() + value.getTimezoneOffset() * 60000) : value; + } + + }); + + DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <div></div> + **/ + tpl:'<div class="editable-date well"></div>', + /** + @property inputclass + @default null + **/ + inputclass: null, + /** + Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br> + Possible tokens are: <code>d, dd, m, mm, yy, yyyy, h, i</code> + + @property format + @type string + @default yyyy-mm-dd hh:ii + **/ + format:'yyyy-mm-dd hh:ii', + formatType:'standard', + /** + Format used for displaying date. Also applied when converting date from element's text on init. + If not specified equals to <code>format</code> + + @property viewformat + @type string + @default null + **/ + viewformat: null, + /** + Configuration of datetimepicker. + Full list of options: https://github.com/smalot/bootstrap-datetimepicker + + @property datetimepicker + @type object + @default { } + **/ + datetimepicker:{ + todayHighlight: false, + autoclose: false + }, + /** + Text shown as clear date button. + If <code>false</code> clear button will not be rendered. + + @property clear + @type boolean|string + @default 'x clear' + **/ + clear: '× clear' + }); + + $.fn.editabletypes.datetime = DateTime; + +}(window.jQuery)); +/** +Bootstrap datetimefield input - datetime input for inline mode. +Shows normal <input type="text"> and binds popup datetimepicker. +Automatically shown in inline mode. + +@class datetimefield +@extends datetime + +**/ +(function ($) { + "use strict"; + + var DateTimeField = function (options) { + this.init('datetimefield', options, DateTimeField.defaults); + this.initPicker(options, DateTimeField.defaults); + }; + + $.fn.editableutils.inherit(DateTimeField, $.fn.editabletypes.datetime); + + $.extend(DateTimeField.prototype, { + render: function () { + this.$input = this.$tpl.find('input'); + this.setClass(); + this.setAttr('placeholder'); + + this.$tpl.datetimepicker(this.options.datetimepicker); + + //need to disable original event handlers + this.$input.off('focus keydown'); + + //update value of datepicker + this.$input.keyup($.proxy(function(){ + this.$tpl.removeData('date'); + this.$tpl.datetimepicker('update'); + }, this)); + + }, + + value2input: function(value) { + this.$input.val(this.value2html(value)); + this.$tpl.datetimepicker('update'); + }, + + input2value: function() { + return this.html2value(this.$input.val()); + }, + + activate: function() { + $.fn.editabletypes.text.prototype.activate.call(this); + }, + + autosubmit: function() { + //reset autosubmit to empty + } + }); + + DateTimeField.defaults = $.extend({}, $.fn.editabletypes.datetime.defaults, { + /** + @property tpl + **/ + tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>', + /** + @property inputclass + @default 'input-medium' + **/ + inputclass: 'input-medium', + + /* datetimepicker config */ + datetimepicker:{ + todayHighlight: false, + autoclose: true + } + }); + + $.fn.editabletypes.datetimefield = DateTimeField; + +}(window.jQuery)); +/** +Typeahead input (bootstrap only). Based on Twitter Bootstrap [typeahead](http://twitter.github.com/bootstrap/javascript.html#typeahead). +Depending on `source` format typeahead operates in two modes: + +* **strings**: + When `source` defined as array of strings, e.g. `['text1', 'text2', 'text3' ...]`. + User can submit one of these strings or any text entered in input (even if it is not matching source). + +* **objects**: + When `source` defined as array of objects, e.g. `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`. + User can submit only values that are in source (otherwise `null` is submitted). This is more like *dropdown* behavior. + +@class typeahead +@extends list +@since 1.4.1 +@final +@example +<a href="#" id="country" data-type="typeahead" data-pk="1" data-url="/post" data-original-title="Input country"></a> +<script> +$(function(){ + $('#country').editable({ + value: 'ru', + source: [ + {value: 'gb', text: 'Great Britain'}, + {value: 'us', text: 'United States'}, + {value: 'ru', text: 'Russia'} + ] + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Constructor = function (options) { + this.init('typeahead', options, Constructor.defaults); + + //overriding objects in config (as by default jQuery extend() is not recursive) + this.options.typeahead = $.extend({}, Constructor.defaults.typeahead, { + //set default methods for typeahead to work with objects + matcher: this.matcher, + sorter: this.sorter, + highlighter: this.highlighter, + updater: this.updater + }, options.typeahead); + }; + + $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.list); + + $.extend(Constructor.prototype, { + renderList: function() { + this.$input = this.$tpl.is('input') ? this.$tpl : this.$tpl.find('input[type="text"]'); + + //set source of typeahead + this.options.typeahead.source = this.sourceData; + + //apply typeahead + this.$input.typeahead(this.options.typeahead); + + //patch some methods in typeahead + var ta = this.$input.data('typeahead'); + ta.render = $.proxy(this.typeaheadRender, ta); + ta.select = $.proxy(this.typeaheadSelect, ta); + ta.move = $.proxy(this.typeaheadMove, ta); + + this.renderClear(); + this.setClass(); + this.setAttr('placeholder'); + }, + + value2htmlFinal: function(value, element) { + if(this.getIsObjects()) { + var items = $.fn.editableutils.itemsByValue(value, this.sourceData); + $(element).text(items.length ? items[0].text : ''); + } else { + $(element).text(value); + } + }, + + html2value: function (html) { + return html ? html : null; + }, + + value2input: function(value) { + if(this.getIsObjects()) { + var items = $.fn.editableutils.itemsByValue(value, this.sourceData); + this.$input.data('value', value).val(items.length ? items[0].text : ''); + } else { + this.$input.val(value); + } + }, + + input2value: function() { + if(this.getIsObjects()) { + var value = this.$input.data('value'), + items = $.fn.editableutils.itemsByValue(value, this.sourceData); + + if(items.length && items[0].text.toLowerCase() === this.$input.val().toLowerCase()) { + return value; + } else { + return null; //entered string not found in source + } + } else { + return this.$input.val(); + } + }, + + /* + if in sourceData values <> texts, typeahead in "objects" mode: + user must pick some value from list, otherwise `null` returned. + if all values == texts put typeahead in "strings" mode: + anything what entered is submited. + */ + getIsObjects: function() { + if(this.isObjects === undefined) { + this.isObjects = false; + for(var i=0; i<this.sourceData.length; i++) { + if(this.sourceData[i].value !== this.sourceData[i].text) { + this.isObjects = true; + break; + } + } + } + return this.isObjects; + }, + + /* + Methods borrowed from text input + */ + activate: $.fn.editabletypes.text.prototype.activate, + renderClear: $.fn.editabletypes.text.prototype.renderClear, + postrender: $.fn.editabletypes.text.prototype.postrender, + toggleClear: $.fn.editabletypes.text.prototype.toggleClear, + clear: function() { + $.fn.editabletypes.text.prototype.clear.call(this); + this.$input.data('value', ''); + }, + + + /* + Typeahead option methods used as defaults + */ + /*jshint eqeqeq:false, curly: false, laxcomma: true, asi: true*/ + matcher: function (item) { + return $.fn.typeahead.Constructor.prototype.matcher.call(this, item.text); + }, + sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item + , text; + + while (item = items.shift()) { + text = item.text; + if (!text.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item); + else if (~text.indexOf(this.query)) caseSensitive.push(item); + else caseInsensitive.push(item); + } + + return beginswith.concat(caseSensitive, caseInsensitive); + }, + highlighter: function (item) { + return $.fn.typeahead.Constructor.prototype.highlighter.call(this, item.text); + }, + updater: function (item) { + this.$element.data('value', item.value); + return item.text; + }, + + + /* + Overwrite typeahead's render method to store objects. + There are a lot of disscussion in bootstrap repo on this point and still no result. + See https://github.com/twitter/bootstrap/issues/5967 + + This function just store item via jQuery data() method instead of attr('data-value') + */ + typeaheadRender: function (items) { + var that = this; + + items = $(items).map(function (i, item) { +// i = $(that.options.item).attr('data-value', item) + i = $(that.options.item).data('item', item); + i.find('a').html(that.highlighter(item)); + return i[0]; + }); + + //add option to disable autoselect of first line + //see https://github.com/twitter/bootstrap/pull/4164 + if (this.options.autoSelect) { + items.first().addClass('active'); + } + this.$menu.html(items); + return this; + }, + + //add option to disable autoselect of first line + //see https://github.com/twitter/bootstrap/pull/4164 + typeaheadSelect: function () { + var val = this.$menu.find('.active').data('item') + if(this.options.autoSelect || val){ + this.$element + .val(this.updater(val)) + .change() + } + return this.hide() + }, + + /* + if autoSelect = false and nothing matched we need extra press onEnter that is not convinient. + This patch fixes it. + */ + typeaheadMove: function (e) { + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + if (!this.$menu.find('.active').length) return + e.preventDefault() + break + + case 38: // up arrow + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + } + + /*jshint eqeqeq: true, curly: true, laxcomma: false, asi: false*/ + + }); + + Constructor.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { + /** + @property tpl + @default <input type="text"> + **/ + tpl:'<input type="text">', + /** + Configuration of typeahead. [Full list of options](http://twitter.github.com/bootstrap/javascript.html#typeahead). + + @property typeahead + @type object + @default null + **/ + typeahead: null, + /** + Whether to show `clear` button + + @property clear + @type boolean + @default true + **/ + clear: true + }); + + $.fn.editabletypes.typeahead = Constructor; + +}(window.jQuery)); \ No newline at end of file diff --git a/dist/bootstrap-editable/js/bootstrap-editable.min.js b/dist/bootstrap-editable/js/bootstrap-editable.min.js new file mode 100644 index 0000000..5b5597e --- /dev/null +++ b/dist/bootstrap-editable/js/bootstrap-editable.min.js @@ -0,0 +1,5 @@ +/*! X-editable - v1.4.4 +* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery +* http://github.com/vitalets/x-editable +* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ +(function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.editableform.defaults,n),this.$div=e(t),this.options.scope||(this.options.scope=this)};t.prototype={constructor:t,initInput:function(){this.input=this.options.input,this.value=this.input.str2value(this.options.value)},initTemplate:function(){this.$form=e(e.fn.editableform.template)},initButtons:function(){var t=this.$form.find(".editable-buttons");t.append(e.fn.editableform.buttons),this.options.showbuttons==="bottom"&&t.addClass("editable-buttons-bottom")},render:function(){this.$loading=e(e.fn.editableform.loading),this.$div.empty().append(this.$loading),this.initTemplate(),this.options.showbuttons?this.initButtons():this.$form.find(".editable-buttons").remove(),this.showLoading(),this.$div.triggerHandler("rendering"),this.initInput(),this.input.prerender(),this.$form.find("div.editable-input").append(this.input.$tpl),this.$div.append(this.$form),e.when(this.input.render()).then(e.proxy(function(){this.options.showbuttons||this.input.autosubmit(),this.$form.find(".editable-cancel").click(e.proxy(this.cancel,this)),this.input.error?(this.error(this.input.error),this.$form.find(".editable-submit").attr("disabled",!0),this.input.$input.attr("disabled",!0),this.$form.submit(function(e){e.preventDefault()})):(this.error(!1),this.input.$input.removeAttr("disabled"),this.$form.find(".editable-submit").removeAttr("disabled"),this.input.value2input(this.value),this.$form.submit(e.proxy(this.submit,this))),this.$div.triggerHandler("rendered"),this.showForm(),this.input.postrender&&this.input.postrender()},this))},cancel:function(){this.$div.triggerHandler("cancel")},showLoading:function(){var e,t;this.$form?(e=this.$form.outerWidth(),t=this.$form.outerHeight(),e&&this.$loading.width(e),t&&this.$loading.height(t),this.$form.hide()):(e=this.$loading.parent().width(),e&&this.$loading.width(e)),this.$loading.show()},showForm:function(e){this.$loading.hide(),this.$form.show(),e!==!1&&this.input.activate(),this.$div.triggerHandler("show")},error:function(t){var n=this.$form.find(".control-group"),r=this.$form.find(".editable-error-block"),i;if(t===!1)n.removeClass(e.fn.editableform.errorGroupClass),r.removeClass(e.fn.editableform.errorBlockClass).empty().hide();else{if(t){i=t.split("\n");for(var s=0;s<i.length;s++)i[s]=e("<div>").text(i[s]).html();t=i.join("<br>")}n.addClass(e.fn.editableform.errorGroupClass),r.addClass(e.fn.editableform.errorBlockClass).html(t).show()}},submit:function(t){t.stopPropagation(),t.preventDefault();var n,r=this.input.input2value();if(n=this.validate(r)){this.error(n),this.showForm();return}if(!this.options.savenochange&&this.input.value2str(r)==this.input.value2str(this.value)){this.$div.triggerHandler("nochange");return}e.when(this.save(r)).done(e.proxy(function(e){var t=typeof this.options.success=="function"?this.options.success.call(this.options.scope,e,r):null;if(t===!1){this.error(!1),this.showForm(!1);return}if(typeof t=="string"){this.error(t),this.showForm();return}t&&typeof t=="object"&&t.hasOwnProperty("newValue")&&(r=t.newValue),this.error(!1),this.value=r,this.$div.triggerHandler("save",{newValue:r,response:e})},this)).fail(e.proxy(function(e){var t;typeof this.options.error=="function"?t=this.options.error.call(this.options.scope,e,r):t=typeof e=="string"?e:e.responseText||e.statusText||"Unknown error!",this.error(t),this.showForm()},this))},save:function(t){var n=this.input.value2submit(t);this.options.pk=e.fn.editableutils.tryParseJson(this.options.pk,!0);var r=typeof this.options.pk=="function"?this.options.pk.call(this.options.scope):this.options.pk,i=!!(typeof this.options.url=="function"||this.options.url&&(this.options.send==="always"||this.options.send==="auto"&&r!==null&&r!==undefined)),s;if(i)return this.showLoading(),s={name:this.options.name||"",value:n,pk:r},typeof this.options.params=="function"?s=this.options.params.call(this.options.scope,s):(this.options.params=e.fn.editableutils.tryParseJson(this.options.params,!0),e.extend(s,this.options.params)),typeof this.options.url=="function"?this.options.url.call(this.options.scope,s):e.ajax(e.extend({url:this.options.url,data:s,type:"POST"},this.options.ajaxOptions))},validate:function(e){e===undefined&&(e=this.value);if(typeof this.options.validate=="function")return this.options.validate.call(this.options.scope,e)},option:function(e,t){e in this.options&&(this.options[e]=t),e==="value"&&this.setValue(t)},setValue:function(e,t){t?this.value=this.input.str2value(e):this.value=e,this.$form&&this.$form.is(":visible")&&this.input.value2input(this.value)}},e.fn.editableform=function(n){var r=arguments;return this.each(function(){var i=e(this),s=i.data("editableform"),o=typeof n=="object"&&n;s||i.data("editableform",s=new t(this,o)),typeof n=="string"&&s[n].apply(s,Array.prototype.slice.call(r,1))})},e.fn.editableform.Constructor=t,e.fn.editableform.defaults={type:"text",url:null,params:null,name:null,pk:null,value:null,send:"auto",validate:null,success:null,error:null,ajaxOptions:null,showbuttons:!0,scope:null,savenochange:!1},e.fn.editableform.template='<form class="form-inline editableform"><div class="control-group"><div><div class="editable-input"></div><div class="editable-buttons"></div></div><div class="editable-error-block"></div></div></form>',e.fn.editableform.loading='<div class="editableform-loading"></div>',e.fn.editableform.buttons='<button type="submit" class="editable-submit">ok</button><button type="button" class="editable-cancel">cancel</button>',e.fn.editableform.errorGroupClass=null,e.fn.editableform.errorBlockClass="editable-error"})(window.jQuery),function(e){"use strict";e.fn.editableutils={inherit:function(e,t){var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e,e.superclass=t.prototype},setCursorPosition:function(e,t){if(e.setSelectionRange)e.setSelectionRange(t,t);else if(e.createTextRange){var n=e.createTextRange();n.collapse(!0),n.moveEnd("character",t),n.moveStart("character",t),n.select()}},tryParseJson:function(e,t){if(typeof e=="string"&&e.length&&e.match(/^[\{\[].*[\}\]]$/))if(t)try{e=(new Function("return "+e))()}catch(n){}finally{return e}else e=(new Function("return "+e))();return e},sliceObj:function(t,n,r){var i,s,o={};if(!e.isArray(n)||!n.length)return o;for(var u=0;u<n.length;u++){i=n[u],t.hasOwnProperty(i)&&(o[i]=t[i]);if(r===!0)continue;s=i.toLowerCase(),t.hasOwnProperty(s)&&(o[i]=t[s])}return o},getConfigData:function(t){var n={};return e.each(t.data(),function(e,t){if(typeof t!="object"||t&&typeof t=="object"&&(t.constructor===Object||t.constructor===Array))n[e]=t}),n},objectKeys:function(e){if(Object.keys)return Object.keys(e);if(e!==Object(e))throw new TypeError("Object.keys called on a non-object");var t=[],n;for(n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t},escape:function(t){return e("<div>").text(t).html()},itemsByValue:function(t,n,r){if(!n||t===null)return[];r=r||"value";var i=e.isArray(t),s=[],o=this;return e.each(n,function(n,u){u.children?s=s.concat(o.itemsByValue(t,u.children,r)):i?e.grep(t,function(e){return e==(u&&typeof u==="object"?u[r]:u)}).length&&s.push(u):t==(u&&typeof u==="object"?u[r]:u)&&s.push(u)}),s},createInput:function(t){var n,r,i,s=t.type;return s==="date"&&(t.mode==="inline"?e.fn.editabletypes.datefield?s="datefield":e.fn.editabletypes.dateuifield&&(s="dateuifield"):e.fn.editabletypes.date?s="date":e.fn.editabletypes.dateui&&(s="dateui"),s==="date"&&!e.fn.editabletypes.date&&(s="combodate")),s==="datetime"&&t.mode==="inline"&&(s="datetimefield"),s==="wysihtml5"&&!e.fn.editabletypes[s]&&(s="textarea"),typeof e.fn.editabletypes[s]=="function"?(n=e.fn.editabletypes[s],r=this.sliceObj(t,this.objectKeys(n.defaults)),i=new n(r),i):(e.error("Unknown type: "+s),!1)}}}(window.jQuery),function(e){"use strict";var t=function(e,t){this.init(e,t)},n=function(e,t){this.init(e,t)};t.prototype={containerName:null,innerCss:null,containerClass:"editable-container editable-popup",init:function(n,r){this.$element=e(n),this.options=e.extend({},e.fn.editableContainer.defaults,r),this.splitOptions(),this.formOptions.scope=this.$element[0],this.initContainer(),this.$element.on("destroyed",e.proxy(function(){this.destroy()},this)),e(document).data("editable-handlers-attached")||(e(document).on("keyup.editable",function(t){t.which===27&&e(".editable-open").editableContainer("hide")}),e(document).on("click.editable",function(n){var r=e(n.target),i,s=[".editable-container",".ui-datepicker-header",".datepicker",".modal-backdrop",".bootstrap-wysihtml5-insert-image-modal",".bootstrap-wysihtml5-insert-link-modal"];if(!e.contains(document.documentElement,n.target))return;if(r.is(document))return;for(i=0;i<s.length;i++)if(r.is(s[i])||r.parents(s[i]).length)return;t.prototype.closeOthers(n.target)}),e(document).data("editable-handlers-attached",!0))},splitOptions:function(){this.containerOptions={},this.formOptions={};if(!e.fn[this.containerName])throw new Error(this.containerName+" not found. Have you included corresponding js file?");var t=e.fn[this.containerName].defaults;for(var n in this.options)n in t?this.containerOptions[n]=this.options[n]:this.formOptions[n]=this.options[n]},tip:function(){return this.container()?this.container().$tip:null},container:function(){return this.$element.data(this.containerDataName||this.containerName)},call:function(){this.$element[this.containerName].apply(this.$element,arguments)},initContainer:function(){this.call(this.containerOptions)},renderForm:function(){this.$form.editableform(this.formOptions).on({save:e.proxy(this.save,this),nochange:e.proxy(function(){this.hide("nochange")},this),cancel:e.proxy(function(){this.hide("cancel")},this),show:e.proxy(this.setPosition,this),rendering:e.proxy(this.setPosition,this),resize:e.proxy(this.setPosition,this),rendered:e.proxy(function(){this.$element.triggerHandler("shown",this)},this)}).editableform("render")},show:function(t){this.$element.addClass("editable-open"),t!==!1&&this.closeOthers(this.$element[0]),this.innerShow(),this.tip().addClass(this.containerClass),this.$form,this.$form=e("<div>"),this.tip().is(this.innerCss)?this.tip().append(this.$form):this.tip().find(this.innerCss).append(this.$form),this.renderForm()},hide:function(e){if(!this.tip()||!this.tip().is(":visible")||!this.$element.hasClass("editable-open"))return;this.$element.removeClass("editable-open"),this.innerHide(),this.$element.triggerHandler("hidden",e||"manual")},innerShow:function(){},innerHide:function(){},toggle:function(e){this.container()&&this.tip()&&this.tip().is(":visible")?this.hide():this.show(e)},setPosition:function(){},save:function(e,t){this.$element.triggerHandler("save",t),this.hide("save")},option:function(e,t){this.options[e]=t,e in this.containerOptions?(this.containerOptions[e]=t,this.setContainerOption(e,t)):(this.formOptions[e]=t,this.$form&&this.$form.editableform("option",e,t))},setContainerOption:function(e,t){this.call("option",e,t)},destroy:function(){this.hide(),this.innerDestroy(),this.$element.off("destroyed"),this.$element.removeData("editableContainer")},innerDestroy:function(){},closeOthers:function(t){e(".editable-open").each(function(n,r){if(r===t||e(r).find(t).length)return;var i=e(r),s=i.data("editableContainer");if(!s)return;s.options.onblur==="cancel"?i.data("editableContainer").hide("onblur"):s.options.onblur==="submit"&&i.data("editableContainer").tip().find("form").submit()})},activate:function(){this.tip&&this.tip().is(":visible")&&this.$form&&this.$form.data("editableform").input.activate()}},e.fn.editableContainer=function(r){var i=arguments;return this.each(function(){var s=e(this),o="editableContainer",u=s.data(o),a=typeof r=="object"&&r,f=a.mode==="inline"?n:t;u||s.data(o,u=new f(this,a)),typeof r=="string"&&u[r].apply(u,Array.prototype.slice.call(i,1))})},e.fn.editableContainer.Popup=t,e.fn.editableContainer.Inline=n,e.fn.editableContainer.defaults={value:null,placement:"top",autohide:!0,onblur:"cancel",anim:!1,mode:"popup"},jQuery.event.special.destroyed={remove:function(e){e.handler&&e.handler()}}}(window.jQuery),function(e){"use strict";e.extend(e.fn.editableContainer.Inline.prototype,e.fn.editableContainer.Popup.prototype,{containerName:"editableform",innerCss:".editable-inline",containerClass:"editable-container editable-inline",initContainer:function(){this.$tip=e("<span></span>"),this.options.anim||(this.options.anim=0)},splitOptions:function(){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,e.proxy(function(){this.$element.show(),this.innerDestroy()},this))},innerDestroy:function(){this.tip()&&this.tip().empty().remove()}})}(window.jQuery),function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.editable.defaults,n,e.fn.editableutils.getConfigData(this.$element)),this.options.selector?this.initLive():this.init()};t.prototype={constructor:t,init:function(){var t=!1,n,r;this.options.name=this.options.name||this.$element.attr("id"),this.options.scope=this.$element[0],this.input=e.fn.editableutils.createInput(this.options);if(!this.input)return;this.options.value===undefined||this.options.value===null?(this.value=this.input.html2value(e.trim(this.$element.html())),t=!0):(this.options.value=e.fn.editableutils.tryParseJson(this.options.value,!0),typeof this.options.value=="string"?this.value=this.input.str2value(this.options.value):this.value=this.options.value),this.$element.addClass("editable"),this.options.toggle!=="manual"?(this.$element.addClass("editable-click"),this.$element.on(this.options.toggle+".editable",e.proxy(function(e){e.preventDefault();if(this.options.toggle==="mouseenter")this.show();else{var t=this.options.toggle!=="click";this.toggle(t)}},this))):this.$element.attr("tabindex",-1);switch(this.options.autotext){case"always":n=!0;break;case"auto":n=!e.trim(this.$element.text()).length&&this.value!==null&&this.value!==undefined&&!t;break;default:n=!1}e.when(n?this.render():!0).then(e.proxy(function(){this.options.disabled?this.disable():this.enable(),this.$element.triggerHandler("init",this)},this))},initLive:function(){var t=this.options.selector;this.options.selector=!1,this.options.autotext="never",this.$element.on(this.options.toggle+".editable",t,e.proxy(function(t){var n=e(t.target);n.data("editable")||(n.hasClass(this.options.emptyclass)&&n.empty(),n.editable(this.options).trigger(t))},this))},render:function(e){if(this.options.display===!1)return;return this.input.value2htmlFinal?this.input.value2html(this.value,this.$element[0],this.options.display,e):typeof this.options.display=="function"?this.options.display.call(this.$element[0],this.value,e):this.input.value2html(this.value,this.$element[0])},enable:function(){this.options.disabled=!1,this.$element.removeClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.options.toggle!=="manual"&&this.$element.attr("tabindex")==="-1"&&this.$element.removeAttr("tabindex")},disable:function(){this.options.disabled=!0,this.hide(),this.$element.addClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.$element.attr("tabindex",-1)},toggleDisabled:function(){this.options.disabled?this.enable():this.disable()},option:function(t,n){if(t&&typeof t=="object"){e.each(t,e.proxy(function(t,n){this.option(e.trim(t),n)},this));return}this.options[t]=n;if(t==="disabled")return n?this.disable():this.enable();t==="value"&&this.setValue(n),this.container&&this.container.option(t,n),this.input.option&&this.input.option(t,n)},handleEmpty:function(t){if(this.options.display===!1)return;this.isEmpty=t!==undefined?t:e.trim(this.$element.text())==="",this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.text(this.options.emptytext),this.options.emptyclass&&this.$element.addClass(this.options.emptyclass)):this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)},show:function(t){if(this.options.disabled)return;if(!this.container){var n=e.extend({},this.options,{value:this.value,input:this.input});this.$element.editableContainer(n),this.$element.on("save.internal",e.proxy(this.save,this)),this.container=this.$element.data("editableContainer")}else if(this.container.tip().is(":visible"))return;this.container.show(t)},hide:function(){this.container&&this.container.hide()},toggle:function(e){this.container&&this.container.tip().is(":visible")?this.hide():this.show(e)},save:function(e,t){if(this.options.unsavedclass){var n=!1;n=n||typeof this.options.url=="function",n=n||this.options.display===!1,n=n||t.response!==undefined,n=n||this.options.savenochange&&this.input.value2str(this.value)!==this.input.value2str(t.newValue),n?this.$element.removeClass(this.options.unsavedclass):this.$element.addClass(this.options.unsavedclass)}this.setValue(t.newValue,!1,t.response)},validate:function(){if(typeof this.options.validate=="function")return this.options.validate.call(this,this.value)},setValue:function(t,n,r){n?this.value=this.input.str2value(t):this.value=t,this.container&&this.container.option("value",this.value),e.when(this.render(r)).then(e.proxy(function(){this.handleEmpty()},this))},activate:function(){this.container&&this.container.activate()},destroy:function(){this.disable(),this.container&&this.container.destroy(),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")}},e.fn.editable=function(n){var r={},i=arguments,s="editable";switch(n){case"validate":return this.each(function(){var t=e(this),n=t.data(s),i;n&&(i=n.validate())&&(r[n.options.name]=i)}),r;case"getValue":return this.each(function(){var t=e(this),n=t.data(s);n&&n.value!==undefined&&n.value!==null&&(r[n.options.name]=n.input.value2submit(n.value))}),r;case"submit":var o=arguments[1]||{},u=this,a=this.editable("validate"),f;return e.isEmptyObject(a)?(f=this.editable("getValue"),o.data&&e.extend(f,o.data),e.ajax(e.extend({url:o.url,data:f,type:"POST"},o.ajaxOptions)).success(function(e){typeof o.success=="function"&&o.success.call(u,e,o)}).error(function(){typeof o.error=="function"&&o.error.apply(u,arguments)})):typeof o.error=="function"&&o.error.call(u,a),this}return this.each(function(){var r=e(this),o=r.data(s),u=typeof n=="object"&&n;o||r.data(s,o=new t(this,u)),typeof n=="string"&&o[n].apply(o,Array.prototype.slice.call(i,1))})},e.fn.editable.defaults={type:"text",disabled:!1,toggle:"click",emptytext:"Empty",autotext:"auto",value:null,display:null,emptyclass:"editable-empty",unsavedclass:"editable-unsaved",selector:null}}(window.jQuery),function(e){"use strict";e.fn.editabletypes={};var t=function(){};t.prototype={init:function(t,n,r){this.type=t,this.options=e.extend({},r,n)},prerender:function(){this.$tpl=e(this.options.tpl),this.$input=this.$tpl,this.$clear=null,this.error=null},render:function(){},value2html:function(t,n){e(n).text(t)},html2value:function(t){return e("<div>").html(t).text()},value2str:function(e){return e},str2value:function(e){return e},value2submit:function(e){return e},value2input:function(e){this.$input.val(e)},input2value:function(){return this.$input.val()},activate:function(){this.$input.is(":visible")&&this.$input.focus()},clear:function(){this.$input.val(null)},escape:function(t){return e("<div>").text(t).html()},autosubmit:function(){},setClass:function(){this.options.inputclass&&this.$input.addClass(this.options.inputclass)},setAttr:function(e){this.options[e]!==undefined&&this.options[e]!==null&&this.$input.attr(e,this.options[e])},option:function(e,t){this.options[e]=t}},t.defaults={tpl:"",inputclass:"input-medium",scope:null,showbuttons:!0},e.extend(e.fn.editabletypes,{abstractinput:t})}(window.jQuery),function(e){"use strict";var t=function(e){};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){var t=e.Deferred();return this.error=null,this.onSourceReady(function(){this.renderList(),t.resolve()},function(){this.error=this.options.sourceError,t.resolve()}),t.promise()},html2value:function(e){return null},value2html:function(t,n,r,i){var s=e.Deferred(),o=function(){typeof r=="function"?r.call(n,t,this.sourceData,i):this.value2htmlFinal(t,n),s.resolve()};return t===null?o.call(this):this.onSourceReady(o,function(){s.resolve()}),s.promise()},onSourceReady:function(t,n){if(e.isArray(this.sourceData)){t.call(this);return}try{this.options.source=e.fn.editableutils.tryParseJson(this.options.source,!1)}catch(r){n.call(this);return}var i=this.options.source;e.isFunction(i)&&(i=i.call(this.options.scope));if(typeof i=="string"){if(this.options.sourceCache){var s=i,o;e(document).data(s)||e(document).data(s,{}),o=e(document).data(s);if(o.loading===!1&&o.sourceData){this.sourceData=o.sourceData,this.doPrepend(),t.call(this);return}if(o.loading===!0){o.callbacks.push(e.proxy(function(){this.sourceData=o.sourceData,this.doPrepend(),t.call(this)},this)),o.err_callbacks.push(e.proxy(n,this));return}o.loading=!0,o.callbacks=[],o.err_callbacks=[]}e.ajax({url:i,type:"get",cache:!1,dataType:"json",success:e.proxy(function(r){o&&(o.loading=!1),this.sourceData=this.makeArray(r),e.isArray(this.sourceData)?(o&&(o.sourceData=this.sourceData,e.each(o.callbacks,function(){this.call()})),this.doPrepend(),t.call(this)):(n.call(this),o&&e.each(o.err_callbacks,function(){this.call()}))},this),error:e.proxy(function(){n.call(this),o&&(o.loading=!1,e.each(o.err_callbacks,function(){this.call()}))},this)})}else this.sourceData=this.makeArray(i),e.isArray(this.sourceData)?(this.doPrepend(),t.call(this)):n.call(this)},doPrepend:function(){if(this.options.prepend===null||this.options.prepend===undefined)return;e.isArray(this.prependData)||(e.isFunction(this.options.prepend)&&(this.options.prepend=this.options.prepend.call(this.options.scope)),this.options.prepend=e.fn.editableutils.tryParseJson(this.options.prepend,!0),typeof this.options.prepend=="string"&&(this.options.prepend={"":this.options.prepend}),this.prependData=this.makeArray(this.options.prepend)),e.isArray(this.prependData)&&e.isArray(this.sourceData)&&(this.sourceData=this.prependData.concat(this.sourceData))},renderList:function(){},value2htmlFinal:function(e,t){},makeArray:function(t){var n,r,i=[],s,o;if(!t||typeof t=="string")return null;if(e.isArray(t)){o=function(e,t){r={value:e,text:t};if(n++>=2)return!1};for(var u=0;u<t.length;u++)s=t[u],typeof s=="object"?(n=0,e.each(s,o),n===1?i.push(r):n>1&&(s.children&&(s.children=this.makeArray(s.children)),i.push(s))):i.push({value:s,text:s})}else e.each(t,function(e,t){i.push({value:e,text:t})});return i},option:function(e,t){this.options[e]=t,e==="source"&&(this.sourceData=null),e==="prepend"&&(this.prependData=null)}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{source:null,prepend:!1,sourceError:"Error when loading list",sourceCache:!0}),e.fn.editabletypes.list=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("text",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.renderClear(),this.setClass(),this.setAttr("placeholder")},activate:function(){this.$input.is(":visible")&&(this.$input.focus(),e.fn.editableutils.setCursorPosition(this.$input.get(0),this.$input.val().length),this.toggleClear&&this.toggleClear())},renderClear:function(){this.options.clear&&(this.$clear=e('<span class="editable-clear-x"></span>'),this.$input.after(this.$clear).css("padding-right",24).keyup(e.proxy(function(t){if(~e.inArray(t.keyCode,[40,38,9,13,27]))return;clearTimeout(this.t);var n=this;this.t=setTimeout(function(){n.toggleClear(t)},100)},this)).parent().css("position","relative"),this.$clear.click(e.proxy(this.clear,this)))},postrender:function(){},toggleClear:function(e){if(!this.$clear)return;var t=this.$input.val().length,n=this.$clear.is(":visible");t&&!n&&this.$clear.show(),!t&&n&&this.$clear.hide()},clear:function(){this.$clear.hide(),this.$input.val("").focus()}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',placeholder:null,clear:!0}),e.fn.editabletypes.text=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("textarea",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.setAttr("placeholder"),this.setAttr("rows"),this.$input.keydown(function(t){t.ctrlKey&&t.which===13&&e(this).closest("form").submit()})},value2html:function(t,n){var r="",i;if(t){i=t.split("\n");for(var s=0;s<i.length;s++)i[s]=e("<div>").text(i[s]).html();r=i.join("<br>")}e(n).html(r)},html2value:function(t){if(!t)return"";var n=new RegExp(String.fromCharCode(10),"g"),r=t.split(/<br\s*\/?>/i);for(var i=0;i<r.length;i++){var s=e("<div>").html(r[i]).text();s=s.replace(n,""),r[i]=s}return r.join("\n")},activate:function(){e.fn.editabletypes.text.prototype.activate.call(this)}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:"<textarea></textarea>",inputclass:"input-large",placeholder:null,rows:7}),e.fn.editabletypes.textarea=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("select",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.list),e.extend(t.prototype,{renderList:function(){this.$input.empty();var t=function(n,r){if(e.isArray(r))for(var i=0;i<r.length;i++)r[i].children?n.append(t(e("<optgroup>",{label:r[i].text}),r[i].children)):n.append(e("<option>",{value:r[i].value}).text(r[i].text));return n};t(this.$input,this.sourceData),this.setClass(),this.$input.on("keydown.editable",function(t){t.which===13&&e(this).closest("form").submit()})},value2htmlFinal:function(t,n){var r="",i=e.fn.editableutils.itemsByValue(t,this.sourceData);i.length&&(r=i[0].text),e(n).text(r)},autosubmit:function(){this.$input.off("keydown.editable").on("change.editable",function(){e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.list.defaults,{tpl:"<select></select>"}),e.fn.editabletypes.select=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("checklist",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.list),e.extend(t.prototype,{renderList:function(){var t,n;this.$tpl.empty();if(!e.isArray(this.sourceData))return;for(var r=0;r<this.sourceData.length;r++)t=e("<label>").append(e("<input>",{type:"checkbox",value:this.sourceData[r].value})).append(e("<span>").text(" "+this.sourceData[r].text)),e("<div>").append(t).appendTo(this.$tpl);this.$input=this.$tpl.find('input[type="checkbox"]'),this.setClass()},value2str:function(t){return e.isArray(t)?t.sort().join(e.trim(this.options.separator)):""},str2value:function(t){var n,r=null;return typeof t=="string"&&t.length?(n=new RegExp("\\s*"+e.trim(this.options.separator)+"\\s*"),r=t.split(n)):e.isArray(t)?r=t:r=[t],r},value2input:function(t){this.$input.prop("checked",!1),e.isArray(t)&&t.length&&this.$input.each(function(n,r){var i=e(r);e.each(t,function(e,t){i.val()==t&&i.prop("checked",!0)})})},input2value:function(){var t=[];return this.$input.filter(":checked").each(function(n,r){t.push(e(r).val())}),t},value2htmlFinal:function(t,n){var r=[],i=e.fn.editableutils.itemsByValue(t,this.sourceData);i.length?(e.each(i,function(t,n){r.push(e.fn.editableutils.escape(n.text))}),e(n).html(r.join("<br>"))):e(n).empty()},activate:function(){this.$input.first().focus()},autosubmit:function(){this.$input.on("keydown",function(t){t.which===13&&e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.list.defaults,{tpl:'<div class="editable-checklist"></div>',inputclass:null,separator:","}),e.fn.editabletypes.checklist=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("password",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),e.extend(t.prototype,{value2html:function(t,n){t?e(n).text("[hidden]"):e(n).empty()},html2value:function(e){return null}}),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="password">'}),e.fn.editabletypes.password=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("email",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="email">'}),e.fn.editabletypes.email=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("url",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="url">'}),e.fn.editabletypes.url=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("tel",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="tel">'}),e.fn.editabletypes.tel=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("number",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),e.extend(t.prototype,{render:function(){t.superclass.render.call(this),this.setAttr("min"),this.setAttr("max"),this.setAttr("step")},postrender:function(){this.$clear&&this.$clear.css({right:24})}}),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="number">',inputclass:"input-mini",min:null,max:null,step:null}),e.fn.editabletypes.number=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("range",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.number),e.extend(t.prototype,{render:function(){this.$input=this.$tpl.filter("input"),this.setClass(),this.setAttr("min"),this.setAttr("max"),this.setAttr("step"),this.$input.on("input",function(){e(this).siblings("output").text(e(this).val())})},activate:function(){this.$input.focus()}}),t.defaults=e.extend({},e.fn.editabletypes.number.defaults,{tpl:'<input type="range"><output style="width: 30px; display: inline-block"></output>',inputclass:"input-medium"}),e.fn.editabletypes.range=t}(window.jQuery),function(e){"use strict";var t=function(n){this.init("select2",n,t.defaults),n.select2=n.select2||{};var r=this,i={placeholder:n.placeholder};this.isMultiple=n.select2.tags||n.select2.multiple,n.select2.tags||(n.source&&(i.data=n.source),i.initSelection=function(t,n){var s=r.str2value(t.val()),o=e.fn.editableutils.itemsByValue(s,i.data,"id");e.isArray(o)&&o.length&&!r.isMultiple&&(o=o[0]),n(o)}),this.options.select2=e.extend({},t.defaults.select2,i,n.select2)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.$input.select2(this.options.select2),"ajax"in this.options.select2,this.isMultiple&&this.$input.on("change",function(){e(this).closest("form").parent().triggerHandler("resize")})},value2html:function(t,n){var r="",i;this.$input?i=this.$input.select2("data"):this.options.select2.tags?i=t:this.options.select2.data&&(i=e.fn.editableutils.itemsByValue(t,this.options.select2.data,"id")),e.isArray(i)?(r=[],e.each(i,function(e,t){r.push(t&&typeof t=="object"?t.text:t)})):i&&(r=i.text),r=e.isArray(r)?r.join(this.options.viewseparator):r,e(n).text(r)},html2value:function(e){return this.options.select2.tags?this.str2value(e,this.options.viewseparator):null},value2input:function(e){this.$input.val(e).trigger("change",!0)},input2value:function(){return this.$input.select2("val")},str2value:function(t,n){if(typeof t!="string"||!this.isMultiple)return t;n=n||this.options.select2.separator||e.fn.select2.defaults.separator;var r,i,s;if(t===null||t.length<1)return null;r=t.split(n);for(i=0,s=r.length;i<s;i+=1)r[i]=e.trim(r[i]);return r},autosubmit:function(){this.$input.on("change",function(t,n){n||e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="hidden">',select2:null,placeholder:null,source:null,viewseparator:", "}),e.fn.editabletypes.select2=t}(window.jQuery),function(e){var t=function(t,n){this.$element=e(t);if(!this.$element.is("input")){e.error("Combodate should be applied to INPUT element");return}this.options=e.extend({},e.fn.combodate.defaults,n,this.$element.data()),this.init()};t.prototype={constructor:t,init:function(){this.map={day:["D","date"],month:["M","month"],year:["Y","year"],hour:["[Hh]","hours"],minute:["m","minutes"],second:["s","seconds"],ampm:["[Aa]",""]},this.$widget=e('<span class="combodate"></span>').html(this.getTemplate()),this.initCombos(),this.$widget.on("change","select",e.proxy(function(){this.$element.val(this.getValue())},this)),this.$widget.find("select").css("width","auto"),this.$element.hide().after(this.$widget),this.setValue(this.$element.val()||this.options.value)},getTemplate:function(){var t=this.options.template;return e.each(this.map,function(e,n){n=n[0];var r=new RegExp(n+"+"),i=n.length>1?n.substring(1,2):n;t=t.replace(r,"{"+i+"}")}),t=t.replace(/ /g," "),e.each(this.map,function(e,n){n=n[0];var r=n.length>1?n.substring(1,2):n;t=t.replace("{"+r+"}",'<select class="'+e+'"></select>')}),t},initCombos:function(){var t=this;e.each(this.map,function(e,n){var r=t.$widget.find("."+e),i,s;r.length&&(t["$"+e]=r,i="fill"+e.charAt(0).toUpperCase()+e.slice(1),s=t[i](),t["$"+e].html(t.renderItems(s)))})},initItems:function(e){var t=[],n;if(this.options.firstItem==="name"){n=moment.relativeTime||moment.langData()._relativeTime;var r=typeof n[e]=="function"?n[e](1,!0,e,!1):n[e];r=r.split(" ").reverse()[0],t.push(["",r])}else this.options.firstItem==="empty"&&t.push(["",""]);return t},renderItems:function(e){var t=[];for(var n=0;n<e.length;n++)t.push('<option value="'+e[n][0]+'">'+e[n][1]+"</option>");return t.join("\n")},fillDay:function(){var e=this.initItems("d"),t,n,r=this.options.template.indexOf("DD")!==-1;for(n=1;n<=31;n++)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillMonth:function(){var e=this.initItems("M"),t,n,r=this.options.template.indexOf("MMMM")!==-1,i=this.options.template.indexOf("MMM")!==-1,s=this.options.template.indexOf("MM")!==-1;for(n=0;n<=11;n++)r?t=moment().month(n).format("MMMM"):i?t=moment().month(n).format("MMM"):s?t=this.leadZero(n+1):t=n+1,e.push([n,t]);return e},fillYear:function(){var e=[],t,n,r=this.options.template.indexOf("YYYY")!==-1;for(n=this.options.maxYear;n>=this.options.minYear;n--)t=r?n:(n+"").substring(2),e[this.options.yearDescending?"push":"unshift"]([n,t]);return e=this.initItems("y").concat(e),e},fillHour:function(){var e=this.initItems("h"),t,n,r=this.options.template.indexOf("h")!==-1,i=this.options.template.indexOf("H")!==-1,s=this.options.template.toLowerCase().indexOf("hh")!==-1,o=r?12:23;for(n=0;n<=o;n++)t=s?this.leadZero(n):n,e.push([n,t]);return e},fillMinute:function(){var e=this.initItems("m"),t,n,r=this.options.template.indexOf("mm")!==-1;for(n=0;n<=59;n+=this.options.minuteStep)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillSecond:function(){var e=this.initItems("s"),t,n,r=this.options.template.indexOf("ss")!==-1;for(n=0;n<=59;n+=this.options.secondStep)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillAmpm:function(){var e=this.options.template.indexOf("a")!==-1,t=this.options.template.indexOf("A")!==-1,n=[["am",e?"am":"AM"],["pm",e?"pm":"PM"]];return n},getValue:function(t){var n,r={},i=this,s=!1;return e.each(this.map,function(e,t){if(e==="ampm")return;var n=e==="day"?1:0;r[e]=i["$"+e]?parseInt(i["$"+e].val(),10):n;if(isNaN(r[e]))return s=!0,!1}),s?"":(this.$ampm&&(r.hour=this.$ampm.val()==="am"?r.hour:r.hour+12,r.hour===24&&(r.hour=0)),n=moment([r.year,r.month,r.day,r.hour,r.minute,r.second]),this.highlight(n),t=t===undefined?this.options.format:t,t===null?n.isValid()?n:null:n.isValid()?n.format(t):"")},setValue:function(t){function s(t,n){var r={};return t.children("option").each(function(t,i){var s=e(i).attr("value"),o;if(s==="")return;o=Math.abs(s-n);if(typeof r.distance=="undefined"||o<r.distance)r={value:s,distance:o}}),r.value}if(!t)return;var n=typeof t=="string"?moment(t,this.options.format):moment(t),r=this,i={};n.isValid()&&(e.each(this.map,function(e,t){if(e==="ampm")return;i[e]=n[t[1]]()}),this.$ampm&&(i.hour>12?(i.hour-=12,i.ampm="pm"):i.ampm="am"),e.each(i,function(e,t){r["$"+e]&&(e==="minute"&&r.options.minuteStep>1&&r.options.roundTime&&(t=s(r["$"+e],t)),e==="second"&&r.options.secondStep>1&&r.options.roundTime&&(t=s(r["$"+e],t)),r["$"+e].val(t))}),this.$element.val(n.format(this.options.format)))},highlight:function(e){e.isValid()?this.options.errorClass?this.$widget.removeClass(this.options.errorClass):this.$widget.find("select").css("border-color",this.borderColor):this.options.errorClass?this.$widget.addClass(this.options.errorClass):(this.borderColor||(this.borderColor=this.$widget.find("select").css("border-color")),this.$widget.find("select").css("border-color","red"))},leadZero:function(e){return e<=9?"0"+e:e},destroy:function(){this.$widget.remove(),this.$element.removeData("combodate").show()}},e.fn.combodate=function(n){var r,i=Array.apply(null,arguments);return i.shift(),n==="getValue"&&this.length&&(r=this.eq(0).data("combodate"))?r.getValue.apply(r,i):this.each(function(){var r=e(this),s=r.data("combodate"),o=typeof n=="object"&&n;s||r.data("combodate",s=new t(this,o)),typeof n=="string"&&typeof s[n]=="function"&&s[n].apply(s,i)})},e.fn.combodate.defaults={format:"DD-MM-YYYY HH:mm",template:"D / MMM / YYYY H : mm",value:null,minYear:1970,maxYear:2015,yearDescending:!0,minuteStep:5,secondStep:1,firstItem:"empty",errorClass:null,roundTime:!0}}(window.jQuery),function(e){"use strict";var t=function(n){this.init("combodate",n,t.defaults),this.options.viewformat||(this.options.viewformat=this.options.format),n.combodate=e.fn.editableutils.tryParseJson(n.combodate,!0),this.options.combodate=e.extend({},t.defaults.combodate,n.combodate,{format:this.options.format,template:this.options.template})};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.$input.combodate(this.options.combodate)},value2html:function(t,n){var r=t?t.format(this.options.viewformat):"";e(n).text(r)},html2value:function(e){return e?moment(e,this.options.viewformat):null},value2str:function(e){return e?e.format(this.options.format):""},str2value:function(e){return e?moment(e,this.options.format):null},value2submit:function(e){return this.value2str(e)},value2input:function(e){this.$input.combodate("setValue",e)},input2value:function(){return this.$input.combodate("getValue",null)},activate:function(){this.$input.siblings(".combodate").find("select").eq(0).focus()},autosubmit:function(){}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',inputclass:null,format:"YYYY-MM-DD",viewformat:null,template:"D / MMM / YYYY",combodate:null}),e.fn.editabletypes.combodate=t}(window.jQuery),function(e){"use strict";e.extend(e.fn.editableform.Constructor.prototype,{initTemplate:function(){this.$form=e(e.fn.editableform.template),this.$form.find(".editable-error-block").addClass("help-block")}}),e.fn.editableform.buttons='<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button><button type="button" class="btn editable-cancel"><i class="icon-remove"></i></button>',e.fn.editableform.errorGroupClass="error",e.fn.editableform.errorBlockClass=null}(window.jQuery),function(e){"use strict";e.extend(e.fn.editableContainer.Popup.prototype,{containerName:"popover",innerCss:e.fn.popover&&e(e.fn.popover.defaults.template).find("p").length?".popover-content p":".popover-content",initContainer:function(){e.extend(this.containerOptions,{trigger:"manual",selector:!1,content:" ",template:e.fn.popover.defaults.template});var t;this.$element.data("template")&&(t=this.$element.data("template"),this.$element.removeData("template")),this.call(this.containerOptions),t&&this.$element.data("template",t)},innerShow:function(){this.call("show")},innerHide:function(){this.call("hide")},innerDestroy:function(){this.call("destroy")},setContainerOption:function(e,t){this.container().options[e]=t},setPosition:function(){(function(){var e=this.tip(),t,n,r,i,s,o;s=typeof this.options.placement=="function"?this.options.placement.call(this,e[0],this.$element[0]):this.options.placement,t=/in/.test(s),e.removeClass("top right bottom left").css({top:0,left:0,display:"block"}),n=this.getPosition(t),r=e[0].offsetWidth,i=e[0].offsetHeight;switch(t?s.split(" ")[1]:s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}e.offset(o).addClass(s).addClass("in")}).call(this.container())}})}(window.jQuery),function(e){"use strict";var t=function(e){this.init("date",e,t.defaults),this.initPicker(e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{initPicker:function(t,n){this.options.viewformat||(this.options.viewformat=this.options.format),this.options.datepicker=e.extend({},n.datepicker,t.datepicker,{format:this.options.viewformat}),this.options.datepicker.language=this.options.datepicker.language||"en",this.dpg=e.fn.datepicker.DPGlobal,this.parsedFormat=this.dpg.parseFormat(this.options.format),this.parsedViewFormat=this.dpg.parseFormat(this.options.viewformat)},render:function(){this.$input.datepicker(this.options.datepicker),this.options.clear&&(this.$clear=e('<a href="#"></a>').html(this.options.clear).click(e.proxy(function(e){e.preventDefault(),e.stopPropagation(),this.clear()},this)),this.$tpl.parent().append(e('<div class="editable-clear">').append(this.$clear)))},value2html:function(e,n){var r=e?this.dpg.formatDate(e,this.parsedViewFormat,this.options.datepicker.language):"";t.superclass.value2html(r,n)},html2value:function(e){return e?this.dpg.parseDate(e,this.parsedViewFormat,this.options.datepicker.language):null},value2str:function(e){return e?this.dpg.formatDate(e,this.parsedFormat,this.options.datepicker.language):""},str2value:function(e){return e?this.dpg.parseDate(e,this.parsedFormat,this.options.datepicker.language):null},value2submit:function(e){return this.value2str(e)},value2input:function(e){this.$input.datepicker("update",e)},input2value:function(){return this.$input.data("datepicker").date},activate:function(){},clear:function(){this.$input.data("datepicker").date=null,this.$input.find(".active").removeClass("active"),this.options.showbuttons||this.$input.closest("form").submit()},autosubmit:function(){this.$input.on("mouseup",".day",function(t){if(e(t.currentTarget).is(".old")||e(t.currentTarget).is(".new"))return;var n=e(this).closest("form");setTimeout(function(){n.submit()},200)})}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<div class="editable-date well"></div>',inputclass:null,format:"yyyy-mm-dd",viewformat:null,datepicker:{weekStart:0,startView:0,minViewMode:0,autoclose:!1},clear:"× clear"}),e.fn.editabletypes.date=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("datefield",e,t.defaults),this.initPicker(e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.date),e.extend(t.prototype,{render:function(){this.$input=this.$tpl.find("input"),this.setClass(),this.setAttr("placeholder"),this.$tpl.datepicker(this.options.datepicker),this.$input.off("focus keydown"),this.$input.keyup(e.proxy(function(){this.$tpl.removeData("date"),this.$tpl.datepicker("update")},this))},value2input:function(e){this.$input.val(e?this.dpg.formatDate(e,this.parsedViewFormat,this.options.datepicker.language):""),this.$tpl.datepicker("update")},input2value:function(){return this.html2value(this.$input.val())},activate:function(){e.fn.editabletypes.text.prototype.activate.call(this)},autosubmit:function(){}}),t.defaults=e.extend({},e.fn.editabletypes.date.defaults,{tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',inputclass:"input-small",datepicker:{weekStart:0,startView:0,minViewMode:0,autoclose:!0}}),e.fn.editabletypes.datefield=t}(window.jQuery),!function(e){function t(){return new Date(Date.UTC.apply(Date,arguments))}function n(){var e=new Date;return t(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())}var r=function(t,n){var r=this;this.element=e(t),this.language=n.language||this.element.data("date-language")||"en",this.language=this.language in i?this.language:this.language.split("-")[0],this.language=this.language in i?this.language:"en",this.isRTL=i[this.language].rtl||!1,this.format=s.parseFormat(n.format||this.element.data("date-format")||i[this.language].format||"mm/dd/yyyy"),this.isInline=!1,this.isInput=this.element.is("input"),this.component=this.element.is(".date")?this.element.find(".add-on, .btn"):!1,this.hasInput=this.component&&this.element.find("input").length,this.component&&this.component.length===0&&(this.component=!1),this.forceParse=!0,"forceParse"in n?this.forceParse=n.forceParse:"dateForceParse"in this.element.data()&&(this.forceParse=this.element.data("date-force-parse")),this.picker=e(s.template),this._buildEvents(),this._attachEvents(),this.isInline?this.picker.addClass("datepicker-inline").appendTo(this.element):this.picker.addClass("datepicker-dropdown dropdown-menu"),this.isRTL&&(this.picker.addClass("datepicker-rtl"),this.picker.find(".prev i, .next i").toggleClass("icon-arrow-left icon-arrow-right")),this.autoclose=!1,"autoclose"in n?this.autoclose=n.autoclose:"dateAutoclose"in this.element.data()&&(this.autoclose=this.element.data("date-autoclose")),this.keyboardNavigation=!0,"keyboardNavigation"in n?this.keyboardNavigation=n.keyboardNavigation:"dateKeyboardNavigation"in this.element.data()&&(this.keyboardNavigation=this.element.data("date-keyboard-navigation")),this.viewMode=this.startViewMode=0;switch(n.startView||this.element.data("date-start-view")){case 2:case"decade":this.viewMode=this.startViewMode=2;break;case 1:case"year":this.viewMode=this.startViewMode=1}this.minViewMode=n.minViewMode||this.element.data("date-min-view-mode")||0;if(typeof this.minViewMode=="string")switch(this.minViewMode){case"months":this.minViewMode=1;break;case"years":this.minViewMode=2;break;default:this.minViewMode=0}this.viewMode=this.startViewMode=Math.max(this.startViewMode,this.minViewMode),this.todayBtn=n.todayBtn||this.element.data("date-today-btn")||!1,this.todayHighlight=n.todayHighlight||this.element.data("date-today-highlight")||!1,this.calendarWeeks=!1,"calendarWeeks"in n?this.calendarWeeks=n.calendarWeeks:"dateCalendarWeeks"in this.element.data()&&(this.calendarWeeks=this.element.data("date-calendar-weeks")),this.calendarWeeks&&this.picker.find("tfoot th.today").attr("colspan",function(e,t){return parseInt(t)+1}),this._allow_update=!1,this.weekStart=(n.weekStart||this.element.data("date-weekstart")||i[this.language].weekStart||0)%7,this.weekEnd=(this.weekStart+6)%7,this.startDate=-Infinity,this.endDate=Infinity,this.daysOfWeekDisabled=[],this.setStartDate(n.startDate||this.element.data("date-startdate")),this.setEndDate(n.endDate||this.element.data("date-enddate")),this.setDaysOfWeekDisabled(n.daysOfWeekDisabled||this.element.data("date-days-of-week-disabled")),this.fillDow(),this.fillMonths(),this._allow_update=!0,this.update(),this.showMode(),this.isInline&&this.show()};r.prototype={constructor:r,_events:[],_secondaryEvents:[],_applyEvents:function(e){for(var t=0,n,r;t<e.length;t++)n=e[t][0],r=e[t][1],n.on(r)},_unapplyEvents:function(e){for(var t=0,n,r;t<e.length;t++)n=e[t][0],r=e[t][1],n.off(r)},_buildEvents:function(){this.isInput?this._events=[[this.element,{focus:e.proxy(this.show,this),keyup:e.proxy(this.update,this),keydown:e.proxy(this.keydown,this)}]]:this.component&&this.hasInput?this._events=[[this.element.find("input"),{focus:e.proxy(this.show,this),keyup:e.proxy(this.update,this),keydown:e.proxy(this.keydown,this)}],[this.component,{click:e.proxy(this.show,this)}]]:this.element.is("div")?this.isInline=!0:this._events=[[this.element,{click:e.proxy(this.show,this)}]],this._secondaryEvents=[[this.picker,{click:e.proxy(this.click,this)}],[e(window),{resize:e.proxy(this.place,this)}],[e(document),{mousedown:e.proxy(function(t){e(t.target).closest(".datepicker.datepicker-inline, .datepicker.datepicker-dropdown").length===0&&this.hide()},this)}]]},_attachEvents:function(){this._detachEvents(),this._applyEvents(this._events)},_detachEvents:function(){this._unapplyEvents(this._events)},_attachSecondaryEvents:function(){this._detachSecondaryEvents(),this._applyEvents(this._secondaryEvents)},_detachSecondaryEvents:function(){this._unapplyEvents(this._secondaryEvents)},show:function(e){this.isInline||this.picker.appendTo("body"),this.picker.show(),this.height=this.component?this.component.outerHeight():this.element.outerHeight(),this.place(),this._attachSecondaryEvents(),e&&e.preventDefault(),this.element.trigger({type:"show",date:this.date})},hide:function(e){if(this.isInline)return;if(!this.picker.is(":visible"))return;this.picker.hide().detach(),this._detachSecondaryEvents(),this.viewMode=this.startViewMode,this.showMode(),this.forceParse&&(this.isInput&&this.element.val()||this.hasInput&&this.element.find("input").val())&&this.setValue(),this.element.trigger({type:"hide",date:this.date})},remove:function(){this.hide(),this._detachEvents(),this._detachSecondaryEvents(),this.picker.remove(),delete this.element.data().datepicker,this.isInput||delete this.element.data().date},getDate:function(){var e=this.getUTCDate();return new Date(e.getTime()+e.getTimezoneOffset()*6e4)},getUTCDate:function(){return this.date},setDate:function(e){this.setUTCDate(new Date(e.getTime()-e.getTimezoneOffset()*6e4))},setUTCDate:function(e){this.date=e,this.setValue()},setValue:function(){var e=this.getFormattedDate();this.isInput?this.element.val(e):(this.component&&this.element.find("input").val(e),this.element.data("date",e))},getFormattedDate:function(e){return e===undefined&&(e=this.format),s.formatDate(this.date,e,this.language)},setStartDate:function(e){this.startDate=e||-Infinity,this.startDate!==-Infinity&&(this.startDate=s.parseDate(this.startDate,this.format,this.language)),this.update(),this.updateNavArrows()},setEndDate:function(e){this.endDate=e||Infinity,this.endDate!==Infinity&&(this.endDate=s.parseDate(this.endDate,this.format,this.language)),this.update(),this.updateNavArrows()},setDaysOfWeekDisabled:function(t){this.daysOfWeekDisabled=t||[],e.isArray(this.daysOfWeekDisabled)||(this.daysOfWeekDisabled=this.daysOfWeekDisabled.split(/,\s*/)),this.daysOfWeekDisabled=e.map(this.daysOfWeekDisabled,function(e){return parseInt(e,10)}),this.update(),this.updateNavArrows()},place:function(){if(this.isInline)return;var t=parseInt(this.element.parents().filter(function(){return e(this).css("z-index")!="auto"}).first().css("z-index"))+10,n=this.component?this.component.parent().offset():this.element.offset(),r=this.component?this.component.outerHeight(!0):this.element.outerHeight(!0);this.picker.css({top:n.top+r,left:n.left,zIndex:t})},_allow_update:!0,update:function(){if(!this._allow_update)return;var e,t=!1;arguments&&arguments.length&&(typeof arguments[0]=="string"||arguments[0]instanceof Date)?(e=arguments[0],t=!0):e=this.isInput?this.element.val():this.element.data("date")||this.element.find("input").val(),this.date=s.parseDate(e,this.format,this.language),t&&this.setValue(),this.date<this.startDate?this.viewDate=new Date(this.startDate):this.date>this.endDate?this.viewDate=new Date(this.endDate):this.viewDate=new Date(this.date),this.fill()},fillDow:function(){var e=this.weekStart,t="<tr>";if(this.calendarWeeks){var n='<th class="cw"> </th>';t+=n,this.picker.find(".datepicker-days thead tr:first-child").prepend(n)}while(e<this.weekStart+7)t+='<th class="dow">'+i[this.language].daysMin[e++%7]+"</th>";t+="</tr>",this.picker.find(".datepicker-days thead").append(t)},fillMonths:function(){var e="",t=0;while(t<12)e+='<span class="month">'+i[this.language].monthsShort[t++]+"</span>";this.picker.find(".datepicker-months td").html(e)},fill:function(){var n=new Date(this.viewDate),r=n.getUTCFullYear(),o=n.getUTCMonth(),u=this.startDate!==-Infinity?this.startDate.getUTCFullYear():-Infinity,a=this.startDate!==-Infinity?this.startDate.getUTCMonth():-Infinity,f=this.endDate!==Infinity?this.endDate.getUTCFullYear():Infinity,l=this.endDate!==Infinity?this.endDate.getUTCMonth():Infinity,c=this.date&&this.date.valueOf(),h=new Date;this.picker.find(".datepicker-days thead th.switch").text(i[this.language].months[o]+" "+r),this.picker.find("tfoot th.today").text(i[this.language].today).toggle(this.todayBtn!==!1),this.updateNavArrows(),this.fillMonths();var p=t(r,o-1,28,0,0,0,0),d=s.getDaysInMonth(p.getUTCFullYear(),p.getUTCMonth());p.setUTCDate(d),p.setUTCDate(d-(p.getUTCDay()-this.weekStart+7)%7);var v=new Date(p);v.setUTCDate(v.getUTCDate()+42),v=v.valueOf();var m=[],g;while(p.valueOf()<v){if(p.getUTCDay()==this.weekStart){m.push("<tr>");if(this.calendarWeeks){var y=new Date(+p+(this.weekStart-p.getUTCDay()-7)%7*864e5),b=new Date(+y+(11-y.getUTCDay())%7*864e5),w=new Date(+(w=t(b.getUTCFullYear(),0,1))+(11-w.getUTCDay())%7*864e5),E=(b-w)/864e5/7+1;m.push('<td class="cw">'+E+"</td>")}}g="";if(p.getUTCFullYear()<r||p.getUTCFullYear()==r&&p.getUTCMonth()<o)g+=" old";else if(p.getUTCFullYear()>r||p.getUTCFullYear()==r&&p.getUTCMonth()>o)g+=" new";this.todayHighlight&&p.getUTCFullYear()==h.getFullYear()&&p.getUTCMonth()==h.getMonth()&&p.getUTCDate()==h.getDate()&&(g+=" today"),c&&p.valueOf()==c&&(g+=" active");if(p.valueOf()<this.startDate||p.valueOf()>this.endDate||e.inArray(p.getUTCDay(),this.daysOfWeekDisabled)!==-1)g+=" disabled";m.push('<td class="day'+g+'">'+p.getUTCDate()+"</td>"),p.getUTCDay()==this.weekEnd&&m.push("</tr>"),p.setUTCDate(p.getUTCDate()+1)}this.picker.find(".datepicker-days tbody").empty().append(m.join(""));var S=this.date&&this.date.getUTCFullYear(),x=this.picker.find(".datepicker-months").find("th:eq(1)").text(r).end().find("span").removeClass("active");S&&S==r&&x.eq(this.date.getUTCMonth()).addClass("active"),(r<u||r>f)&&x.addClass("disabled"),r==u&&x.slice(0,a).addClass("disabled"),r==f&&x.slice(l+1).addClass("disabled"),m="",r=parseInt(r/10,10)*10;var T=this.picker.find(".datepicker-years").find("th:eq(1)").text(r+"-"+(r+9)).end().find("td");r-=1;for(var N=-1;N<11;N++)m+='<span class="year'+(N==-1||N==10?" old":"")+(S==r?" active":"")+(r<u||r>f?" disabled":"")+'">'+r+"</span>",r+=1;T.html(m)},updateNavArrows:function(){if(!this._allow_update)return;var e=new Date(this.viewDate),t=e.getUTCFullYear(),n=e.getUTCMonth();switch(this.viewMode){case 0:this.startDate!==-Infinity&&t<=this.startDate.getUTCFullYear()&&n<=this.startDate.getUTCMonth()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),this.endDate!==Infinity&&t>=this.endDate.getUTCFullYear()&&n>=this.endDate.getUTCMonth()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"});break;case 1:case 2:this.startDate!==-Infinity&&t<=this.startDate.getUTCFullYear()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),this.endDate!==Infinity&&t>=this.endDate.getUTCFullYear()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"})}},click:function(n){n.preventDefault();var r=e(n.target).closest("span, td, th");if(r.length==1)switch(r[0].nodeName.toLowerCase()){case"th":switch(r[0].className){case"switch":this.showMode(1);break;case"prev":case"next":var i=s.modes[this.viewMode].navStep*(r[0].className=="prev"?-1:1);switch(this.viewMode){case 0:this.viewDate=this.moveMonth(this.viewDate,i);break;case 1:case 2:this.viewDate=this.moveYear(this.viewDate,i)}this.fill();break;case"today":var o=new Date;o=t(o.getFullYear(),o.getMonth(),o.getDate(),0,0,0),this.showMode(-2);var u=this.todayBtn=="linked"?null:"view";this._setDate(o,u)}break;case"span":if(!r.is(".disabled")){this.viewDate.setUTCDate(1);if(r.is(".month")){var a=1,f=r.parent().find("span").index(r),l=this.viewDate.getUTCFullYear();this.viewDate.setUTCMonth(f),this.element.trigger({type:"changeMonth",date:this.viewDate}),this.minViewMode==1&&this._setDate(t(l,f,a,0,0,0,0))}else{var l=parseInt(r.text(),10)||0,a=1,f=0;this.viewDate.setUTCFullYear(l),this.element.trigger({type:"changeYear",date:this.viewDate}),this.minViewMode==2&&this._setDate(t(l,f,a,0,0,0,0))}this.showMode(-1),this.fill()}break;case"td":if(r.is(".day")&&!r.is(".disabled")){var a=parseInt(r.text(),10)||1,l=this.viewDate.getUTCFullYear(),f=this.viewDate.getUTCMonth();r.is(".old")?f===0?(f=11,l-=1):f-=1:r.is(".new")&&(f==11?(f=0,l+=1):f+=1),this._setDate(t(l,f,a,0,0,0,0))}}},_setDate:function(e,t){if(!t||t=="date")this.date=e;if(!t||t=="view")this.viewDate=e;this.fill(),this.setValue(),this.element.trigger({type:"changeDate",date:this.date});var n;this.isInput?n=this.element:this.component&&(n=this.element.find("input")),n&&(n.change(),this.autoclose&&(!t||t=="date")&&this.hide())},moveMonth:function(e,t){if(!t)return e;var n=new Date(e.valueOf()),r=n.getUTCDate(),i=n.getUTCMonth(),s=Math.abs(t),o,u;t=t>0?1:-1;if(s==1){u=t==-1?function(){return n.getUTCMonth()==i}:function(){return n.getUTCMonth()!=o},o=i+t,n.setUTCMonth(o);if(o<0||o>11)o=(o+12)%12}else{for(var a=0;a<s;a++)n=this.moveMonth(n,t);o=n.getUTCMonth(),n.setUTCDate(r),u=function(){return o!=n.getUTCMonth()}}while(u())n.setUTCDate(--r),n.setUTCMonth(o);return n},moveYear:function(e,t){return this.moveMonth(e,t*12)},dateWithinRange:function(e){return e>=this.startDate&&e<=this.endDate},keydown:function(e){if(this.picker.is(":not(:visible)")){e.keyCode==27&&this.show();return}var t=!1,n,r,i,s,o;switch(e.keyCode){case 27:this.hide(),e.preventDefault();break;case 37:case 39:if(!this.keyboardNavigation)break;n=e.keyCode==37?-1:1,e.ctrlKey?(s=this.moveYear(this.date,n),o=this.moveYear(this.viewDate,n)):e.shiftKey?(s=this.moveMonth(this.date,n),o=this.moveMonth(this.viewDate,n)):(s=new Date(this.date),s.setUTCDate(this.date.getUTCDate()+n),o=new Date(this.viewDate),o.setUTCDate(this.viewDate.getUTCDate()+n)),this.dateWithinRange(s)&&(this.date=s,this.viewDate=o,this.setValue(),this.update(),e.preventDefault(),t=!0);break;case 38:case 40:if(!this.keyboardNavigation)break;n=e.keyCode==38?-1:1,e.ctrlKey?(s=this.moveYear(this.date,n),o=this.moveYear(this.viewDate,n)):e.shiftKey?(s=this.moveMonth(this.date,n),o=this.moveMonth(this.viewDate,n)):(s=new Date(this.date),s.setUTCDate(this.date.getUTCDate()+n*7),o=new Date(this.viewDate),o.setUTCDate(this.viewDate.getUTCDate()+n*7)),this.dateWithinRange(s)&&(this.date=s,this.viewDate=o,this.setValue(),this.update(),e.preventDefault(),t=!0);break;case 13:this.hide(),e.preventDefault();break;case 9:this.hide()}if(t){this.element.trigger({type:"changeDate",date:this.date});var u;this.isInput?u=this.element:this.component&&(u=this.element.find("input")),u&&u.change()}},showMode:function(e){e&&(this.viewMode=Math.max(this.minViewMode,Math.min(2,this.viewMode+e))),this.picker.find(">div").hide().filter(".datepicker-"+s.modes[this.viewMode].clsName).css("display","block"),this.updateNavArrows()}},e.fn.datepicker=function(t){var n=Array.apply(null,arguments);return n.shift(),this.each(function(){var i=e(this),s=i.data("datepicker"),o=typeof t=="object"&&t;s||i.data("datepicker",s=new r(this,e.extend({},e.fn.datepicker.defaults,o))),typeof t=="string"&&typeof s[t]=="function"&&s[t].apply(s,n)})},e.fn.datepicker.defaults={},e.fn.datepicker.Constructor=r;var i=e.fn.datepicker.dates={en:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat","Sun"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa","Su"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today"}},s={modes:[{clsName:"days",navFnc:"Month",navStep:1},{clsName:"months",navFnc:"FullYear",navStep:1},{clsName:"years",navFnc:"FullYear",navStep:10}],isLeapYear:function(e){return e%4===0&&e%100!==0||e%400===0},getDaysInMonth:function(e,t){return[31,s.isLeapYear(e)?29:28,31,30,31,30,31,31,30,31,30,31][t]},validParts:/dd?|DD?|mm?|MM?|yy(?:yy)?/g,nonpunctuation:/[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,parseFormat:function(e){var t=e.replace(this.validParts,"\0").split("\0"),n=e.match(this.validParts);if(!t||!t.length||!n||n.length===0)throw new Error("Invalid date format.");return{separators:t,parts:n}},parseDate:function(n,s,o){if(n instanceof Date)return n;if(/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(n)){var u=/([\-+]\d+)([dmwy])/,a=n.match(/([\-+]\d+)([dmwy])/g),f,l;n=new Date;for(var c=0;c<a.length;c++){f=u.exec(a[c]),l=parseInt(f[1]);switch(f[2]){case"d":n.setUTCDate(n.getUTCDate()+l);break;case"m":n=r.prototype.moveMonth.call(r.prototype,n,l);break;case"w":n.setUTCDate(n.getUTCDate()+l*7);break;case"y":n=r.prototype.moveYear.call(r.prototype,n,l)}}return t(n.getUTCFullYear(),n.getUTCMonth(),n.getUTCDate(),0,0,0)}var a=n&&n.match(this.nonpunctuation)||[],n=new Date,h={},p=["yyyy","yy","M","MM","m","mm","d","dd"],d={yyyy:function(e,t){return e.setUTCFullYear(t)},yy:function(e,t){return e.setUTCFullYear(2e3+t)},m:function(e,t){t-=1;while(t<0)t+=12;t%=12,e.setUTCMonth(t);while(e.getUTCMonth()!=t)e.setUTCDate(e.getUTCDate()-1);return e},d:function(e,t){return e.setUTCDate(t)}},v,m,f;d.M=d.MM=d.mm=d.m,d.dd=d.d,n=t(n.getFullYear(),n.getMonth(),n.getDate(),0,0,0);var g=s.parts.slice();a.length!=g.length&&(g=e(g).filter(function(t,n){return e.inArray(n,p)!==-1}).toArray());if(a.length==g.length){for(var c=0,y=g.length;c<y;c++){v=parseInt(a[c],10),f=g[c];if(isNaN(v))switch(f){case"MM":m=e(i[o].months).filter(function(){var e=this.slice(0,a[c].length),t=a[c].slice(0,e.length);return e==t}),v=e.inArray(m[0],i[o].months)+1;break;case"M":m=e(i[o].monthsShort).filter(function(){var e=this.slice(0,a[c].length),t=a[c].slice(0,e.length);return e==t}),v=e.inArray(m[0],i[o].monthsShort)+1}h[f]=v}for(var c=0,b;c<p.length;c++)b=p[c],b in h&&!isNaN(h[b])&&d[b](n,h[b])}return n},formatDate:function(t,n,r){var s={d:t.getUTCDate(),D:i[r].daysShort[t.getUTCDay()],DD:i[r].days[t.getUTCDay()],m:t.getUTCMonth()+1,M:i[r].monthsShort[t.getUTCMonth()],MM:i[r].months[t.getUTCMonth()],yy:t.getUTCFullYear().toString().substring(2),yyyy:t.getUTCFullYear()};s.dd=(s.d<10?"0":"")+s.d,s.mm=(s.m<10?"0":"")+s.m;var t=[],o=e.extend([],n.separators);for(var u=0,a=n.parts.length;u<a;u++)o.length&&t.push(o.shift()),t.push(s[n.parts[u]]);return t.join("")},headTemplate:'<thead><tr><th class="prev"><i class="icon-arrow-left"/></th><th colspan="5" class="switch"></th><th class="next"><i class="icon-arrow-right"/></th></tr></thead>',contTemplate:'<tbody><tr><td colspan="7"></td></tr></tbody>',footTemplate:'<tfoot><tr><th colspan="7" class="today"></th></tr></tfoot>'};s.template='<div class="datepicker"><div class="datepicker-days"><table class=" table-condensed">'+s.headTemplate+"<tbody></tbody>"+s.footTemplate+"</table>"+"</div>"+'<div class="datepicker-months">'+'<table class="table-condensed">'+s.headTemplate+s.contTemplate+s.footTemplate+"</table>"+"</div>"+'<div class="datepicker-years">'+'<table class="table-condensed">'+s.headTemplate+s.contTemplate+s.footTemplate+"</table>"+"</div>"+"</div>",e.fn.datepicker.DPGlobal=s}(window.jQuery),function(e){"use strict";var t=function(e){this.init("datetime",e,t.defaults),this.initPicker(e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{initPicker:function(t,n){this.options.viewformat||(this.options.viewformat=this.options.format),this.options.datetimepicker=e.extend({},n.datetimepicker,t.datetimepicker,{format:this.options.viewformat}),this.options.datetimepicker.language=this.options.datetimepicker.language||"en",this.dpg=e.fn.datetimepicker.DPGlobal,this.parsedFormat=this.dpg.parseFormat(this.options.format,this.options.formatType),this.parsedViewFormat=this.dpg.parseFormat(this.options.viewformat,this.options.formatType),this.options.datetimepicker.startView=this.options.startView,this.options.datetimepicker.minView=this.options.minView,this.options.datetimepicker.maxView=this.options.maxView},render:function(){this.$input.datetimepicker(this.options.datetimepicker),this.options.clear&&(this.$clear=e('<a href="#"></a>').html(this.options.clear).click(e.proxy(function(e){e.preventDefault(),e.stopPropagation(),this.clear()},this)),this.$tpl.parent().append(e('<div class="editable-clear">').append(this.$clear)))},value2html:function(e,n){var r=e?this.dpg.formatDate(this.toUTC(e),this.parsedViewFormat,this.options.datetimepicker.language,this.options.formatType):"";if(!n)return r;t.superclass.value2html(r,n)},html2value:function(e){var t=e?this.dpg.parseDate(e,this.parsedViewFormat,this.options.datetimepicker.language,this.options.formatType):null;return t?this.fromUTC(t):null},value2str:function(e){return e?this.dpg.formatDate(this.toUTC(e),this.parsedFormat,this.options.datetimepicker.language,this.options.formatType):""},str2value:function(e){var t=e?this.dpg.parseDate(e,this.parsedFormat,this.options.datetimepicker.language,this.options.formatType):null;return t?this.fromUTC(t):null},value2submit:function(e){return this.value2str(e)},value2input:function(e){e&&this.$input.data("datetimepicker").setDate(e)},input2value:function(){var e=this.$input.data("datetimepicker");return e.date?e.getDate():null},activate:function(){},clear:function(){this.$input.data("datetimepicker").date=null,this.$input.find(".active").removeClass("active"),this.options.showbuttons||this.$input.closest("form").submit()},autosubmit:function(){this.$input.on("mouseup",".minute",function(t){var n=e(this).closest("form");setTimeout(function(){n.submit()},200)})},toUTC:function(e){return e?new Date(e.valueOf()-e.getTimezoneOffset()*6e4):e},fromUTC:function(e){return e?new Date(e.valueOf()+e.getTimezoneOffset()*6e4):e}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<div class="editable-date well"></div>',inputclass:null,format:"yyyy-mm-dd hh:ii",formatType:"standard",viewformat:null,datetimepicker:{todayHighlight:!1,autoclose:!1},clear:"× clear"}),e.fn.editabletypes.datetime=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("datetimefield",e,t.defaults),this.initPicker(e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.datetime),e.extend(t.prototype,{render:function(){this.$input=this.$tpl.find("input"),this.setClass(),this.setAttr("placeholder"),this.$tpl.datetimepicker(this.options.datetimepicker),this.$input.off("focus keydown"),this.$input.keyup(e.proxy(function(){this.$tpl.removeData("date"),this.$tpl.datetimepicker("update")},this))},value2input:function(e){this.$input.val(this.value2html(e)),this.$tpl.datetimepicker("update")},input2value:function(){return this.html2value(this.$input.val())},activate:function(){e.fn.editabletypes.text.prototype.activate.call(this)},autosubmit:function(){}}),t.defaults=e.extend({},e.fn.editabletypes.datetime.defaults,{tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',inputclass:"input-medium",datetimepicker:{todayHighlight:!1,autoclose:!0}}),e.fn.editabletypes.datetimefield=t}(window.jQuery),function(e){"use strict";var t=function(n){this.init("typeahead",n,t.defaults),this.options.typeahead=e.extend({},t.defaults.typeahead,{matcher:this.matcher,sorter:this.sorter,highlighter:this.highlighter,updater:this.updater},n.typeahead)};e.fn.editableutils.inherit(t,e.fn.editabletypes.list),e.extend(t.prototype,{renderList:function(){this.$input=this.$tpl.is("input")?this.$tpl:this.$tpl.find('input[type="text"]'),this.options.typeahead.source=this.sourceData,this.$input.typeahead(this.options.typeahead);var t=this.$input.data("typeahead");t.render=e.proxy(this.typeaheadRender,t),t.select=e.proxy(this.typeaheadSelect,t),t.move=e.proxy(this.typeaheadMove,t),this.renderClear(),this.setClass(),this.setAttr("placeholder")},value2htmlFinal:function(t,n){if(this.getIsObjects()){var r=e.fn.editableutils.itemsByValue(t,this.sourceData);e(n).text(r.length?r[0].text:"")}else e(n).text(t)},html2value:function(e){return e?e:null},value2input:function(t){if(this.getIsObjects()){var n=e.fn.editableutils.itemsByValue(t,this.sourceData);this.$input.data("value",t).val(n.length?n[0].text:"")}else this.$input.val(t)},input2value:function(){if(this.getIsObjects()){var t=this.$input.data("value"),n=e.fn.editableutils.itemsByValue(t,this.sourceData);return n.length&&n[0].text.toLowerCase()===this.$input.val().toLowerCase()?t:null}return this.$input.val()},getIsObjects:function(){if(this.isObjects===undefined){this.isObjects=!1;for(var e=0;e<this.sourceData.length;e++)if(this.sourceData[e].value!==this.sourceData[e].text){this.isObjects=!0;break}}return this.isObjects},activate:e.fn.editabletypes.text.prototype.activate,renderClear:e.fn.editabletypes.text.prototype.renderClear,postrender:e.fn.editabletypes.text.prototype.postrender,toggleClear:e.fn.editabletypes.text.prototype.toggleClear,clear:function(){e.fn.editabletypes.text.prototype.clear.call(this),this.$input.data("value","")},matcher:function(t){return e.fn.typeahead.Constructor.prototype.matcher.call(this,t.text)},sorter:function(e){var t=[],n=[],r=[],i,s;while(i=e.shift())s=i.text,s.toLowerCase().indexOf(this.query.toLowerCase())?~s.indexOf(this.query)?n.push(i):r.push(i):t.push(i);return t.concat(n,r)},highlighter:function(t){return e.fn.typeahead.Constructor.prototype.highlighter.call(this,t.text)},updater:function(e){return this.$element.data("value",e.value),e.text},typeaheadRender:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).data("item",r),t.find("a").html(n.highlighter(r)),t[0]}),this.options.autoSelect&&t.first().addClass("active"),this.$menu.html(t),this},typeaheadSelect:function(){var e=this.$menu.find(".active").data("item");return(this.options.autoSelect||e)&&this.$element.val(this.updater(e)).change(),this.hide()},typeaheadMove:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:if(!this.$menu.find(".active").length)return;e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()}}),t.defaults=e.extend({},e.fn.editabletypes.list.defaults,{tpl:'<input type="text">',typeahead:null,clear:!0}),e.fn.editabletypes.typeahead=t}(window.jQuery); \ No newline at end of file diff --git a/dist/inputs-ext/address/address.css b/dist/inputs-ext/address/address.css new file mode 100644 index 0000000..f37a43b --- /dev/null +++ b/dist/inputs-ext/address/address.css @@ -0,0 +1,9 @@ +.editable-address { + display: block; + margin-bottom: 5px; +} + +.editable-address span { + width: 70px; + display: inline-block; +} \ No newline at end of file diff --git a/dist/inputs-ext/address/address.js b/dist/inputs-ext/address/address.js new file mode 100644 index 0000000..de3af81 --- /dev/null +++ b/dist/inputs-ext/address/address.js @@ -0,0 +1,170 @@ +/** +Address editable input. +Internally value stored as {city: "Moscow", street: "Lenina", building: "15"} + +@class address +@extends abstractinput +@final +@example +<a href="#" id="address" data-type="address" data-pk="1">awesome</a> +<script> +$(function(){ + $('#address').editable({ + url: '/post', + title: 'Enter city, street and building #', + value: { + city: "Moscow", + street: "Lenina", + building: "15" + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Address = function (options) { + this.init('address', options, Address.defaults); + }; + + //inherit from Abstract input + $.fn.editableutils.inherit(Address, $.fn.editabletypes.abstractinput); + + $.extend(Address.prototype, { + /** + Renders input from tpl + + @method render() + **/ + render: function() { + this.$input = this.$tpl.find('input'); + }, + + /** + Default method to show value in element. Can be overwritten by display option. + + @method value2html(value, element) + **/ + value2html: function(value, element) { + if(!value) { + $(element).empty(); + return; + } + var html = $('<div>').text(value.city).html() + ', ' + $('<div>').text(value.street).html() + ' st., bld. ' + $('<div>').text(value.building).html(); + $(element).html(html); + }, + + /** + Gets value from element's html + + @method html2value(html) + **/ + html2value: function(html) { + /* + you may write parsing method to get value by element's html + e.g. "Moscow, st. Lenina, bld. 15" => {city: "Moscow", street: "Lenina", building: "15"} + but for complex structures it's not recommended. + Better set value directly via javascript, e.g. + editable({ + value: { + city: "Moscow", + street: "Lenina", + building: "15" + } + }); + */ + return null; + }, + + /** + Converts value to string. + It is used in internal comparing (not for sending to server). + + @method value2str(value) + **/ + value2str: function(value) { + var str = ''; + if(value) { + for(var k in value) { + str = str + k + ':' + value[k] + ';'; + } + } + return str; + }, + + /* + Converts string to value. Used for reading value from 'data-value' attribute. + + @method str2value(str) + */ + str2value: function(str) { + /* + this is mainly for parsing value defined in data-value attribute. + If you will always set value by javascript, no need to overwrite it + */ + return str; + }, + + /** + Sets value of input. + + @method value2input(value) + @param {mixed} value + **/ + value2input: function(value) { + if(!value) { + return; + } + this.$input.filter('[name="city"]').val(value.city); + this.$input.filter('[name="street"]').val(value.street); + this.$input.filter('[name="building"]').val(value.building); + }, + + /** + Returns value of input. + + @method input2value() + **/ + input2value: function() { + return { + city: this.$input.filter('[name="city"]').val(), + street: this.$input.filter('[name="street"]').val(), + building: this.$input.filter('[name="building"]').val() + }; + }, + + /** + Activates input: sets focus on the first field. + + @method activate() + **/ + activate: function() { + this.$input.filter('[name="city"]').focus(); + }, + + /** + Attaches handler to submit form in case of 'showbuttons=false' mode + + @method autosubmit() + **/ + autosubmit: function() { + this.$input.keydown(function (e) { + if (e.which === 13) { + $(this).closest('form').submit(); + } + }); + } + }); + + Address.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + tpl: '<div class="editable-address"><label><span>City: </span><input type="text" name="city" class="input-small"></label></div>'+ + '<div class="editable-address"><label><span>Street: </span><input type="text" name="street" class="input-small"></label></div>'+ + '<div class="editable-address"><label><span>Building: </span><input type="text" name="building" class="input-mini"></label></div>', + + inputclass: '' + }); + + $.fn.editabletypes.address = Address; + +}(window.jQuery)); \ No newline at end of file diff --git a/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.css b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.css new file mode 100644 index 0000000..44ed777 --- /dev/null +++ b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.css @@ -0,0 +1,102 @@ +ul.wysihtml5-toolbar { + margin: 0; + padding: 0; + display: block; +} + +ul.wysihtml5-toolbar::after { + clear: both; + display: table; + content: ""; +} + +ul.wysihtml5-toolbar > li { + float: left; + display: list-item; + list-style: none; + margin: 0 5px 10px 0; +} + +ul.wysihtml5-toolbar a[data-wysihtml5-command=bold] { + font-weight: bold; +} + +ul.wysihtml5-toolbar a[data-wysihtml5-command=italic] { + font-style: italic; +} + +ul.wysihtml5-toolbar a[data-wysihtml5-command=underline] { + text-decoration: underline; +} + +ul.wysihtml5-toolbar a.btn.wysihtml5-command-active { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #E6E6E6; + background-color: #D9D9D9; + outline: 0; +} + +ul.wysihtml5-commands-disabled .dropdown-menu { + display: none !important; +} + +ul.wysihtml5-toolbar div.wysihtml5-colors { + display:block; + width: 50px; + height: 20px; + margin-top: 2px; + margin-left: 5px; + position: absolute; + pointer-events: none; +} + +ul.wysihtml5-toolbar a.wysihtml5-colors-title { + padding-left: 70px; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="black"] { + background: black !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="silver"] { + background: silver !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="gray"] { + background: gray !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="maroon"] { + background: maroon !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="red"] { + background: red !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="purple"] { + background: purple !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="green"] { + background: green !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="olive"] { + background: olive !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="navy"] { + background: navy !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="blue"] { + background: blue !important; +} + +ul.wysihtml5-toolbar div[data-wysihtml5-command-value="orange"] { + background: orange !important; +} diff --git a/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.js b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.js new file mode 100644 index 0000000..695d264 --- /dev/null +++ b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.js @@ -0,0 +1,499 @@ +!function($, wysi) { + "use strict"; + + var tpl = { + "font-styles": function(locale, options) { + var size = (options && options.size) ? ' btn-'+options.size : ''; + return "<li class='dropdown'>" + + "<a class='btn dropdown-toggle" + size + "' data-toggle='dropdown' href='#'>" + + "<i class='icon-font'></i> <span class='current-font'>" + locale.font_styles.normal + "</span> <b class='caret'></b>" + + "</a>" + + "<ul class='dropdown-menu'>" + + "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='div' tabindex='-1'>" + locale.font_styles.normal + "</a></li>" + + "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h1' tabindex='-1'>" + locale.font_styles.h1 + "</a></li>" + + "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h2' tabindex='-1'>" + locale.font_styles.h2 + "</a></li>" + + "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h3' tabindex='-1'>" + locale.font_styles.h3 + "</a></li>" + + "</ul>" + + "</li>"; + }, + + "emphasis": function(locale, options) { + var size = (options && options.size) ? ' btn-'+options.size : ''; + return "<li>" + + "<div class='btn-group'>" + + "<a class='btn" + size + "' data-wysihtml5-command='bold' title='CTRL+B' tabindex='-1'>" + locale.emphasis.bold + "</a>" + + "<a class='btn" + size + "' data-wysihtml5-command='italic' title='CTRL+I' tabindex='-1'>" + locale.emphasis.italic + "</a>" + + "<a class='btn" + size + "' data-wysihtml5-command='underline' title='CTRL+U' tabindex='-1'>" + locale.emphasis.underline + "</a>" + + "</div>" + + "</li>"; + }, + + "lists": function(locale, options) { + var size = (options && options.size) ? ' btn-'+options.size : ''; + return "<li>" + + "<div class='btn-group'>" + + "<a class='btn" + size + "' data-wysihtml5-command='insertUnorderedList' title='" + locale.lists.unordered + "' tabindex='-1'><i class='icon-list'></i></a>" + + "<a class='btn" + size + "' data-wysihtml5-command='insertOrderedList' title='" + locale.lists.ordered + "' tabindex='-1'><i class='icon-th-list'></i></a>" + + "<a class='btn" + size + "' data-wysihtml5-command='Outdent' title='" + locale.lists.outdent + "' tabindex='-1'><i class='icon-indent-right'></i></a>" + + "<a class='btn" + size + "' data-wysihtml5-command='Indent' title='" + locale.lists.indent + "' tabindex='-1'><i class='icon-indent-left'></i></a>" + + "</div>" + + "</li>"; + }, + + "link": function(locale, options) { + var size = (options && options.size) ? ' btn-'+options.size : ''; + return "<li>" + + "<div class='bootstrap-wysihtml5-insert-link-modal modal hide fade'>" + + "<div class='modal-header'>" + + "<a class='close' data-dismiss='modal'>×</a>" + + "<h3>" + locale.link.insert + "</h3>" + + "</div>" + + "<div class='modal-body'>" + + "<input value='http://' class='bootstrap-wysihtml5-insert-link-url input-xlarge'>" + + "</div>" + + "<div class='modal-footer'>" + + "<a href='#' class='btn' data-dismiss='modal'>" + locale.link.cancel + "</a>" + + "<a href='#' class='btn btn-primary' data-dismiss='modal'>" + locale.link.insert + "</a>" + + "</div>" + + "</div>" + + "<a class='btn" + size + "' data-wysihtml5-command='createLink' title='" + locale.link.insert + "' tabindex='-1'><i class='icon-share'></i></a>" + + "</li>"; + }, + + "image": function(locale, options) { + var size = (options && options.size) ? ' btn-'+options.size : ''; + return "<li>" + + "<div class='bootstrap-wysihtml5-insert-image-modal modal hide fade'>" + + "<div class='modal-header'>" + + "<a class='close' data-dismiss='modal'>×</a>" + + "<h3>" + locale.image.insert + "</h3>" + + "</div>" + + "<div class='modal-body'>" + + "<input value='http://' class='bootstrap-wysihtml5-insert-image-url input-xlarge'>" + + "</div>" + + "<div class='modal-footer'>" + + "<a href='#' class='btn' data-dismiss='modal'>" + locale.image.cancel + "</a>" + + "<a href='#' class='btn btn-primary' data-dismiss='modal'>" + locale.image.insert + "</a>" + + "</div>" + + "</div>" + + "<a class='btn" + size + "' data-wysihtml5-command='insertImage' title='" + locale.image.insert + "' tabindex='-1'><i class='icon-picture'></i></a>" + + "</li>"; + }, + + "html": function(locale, options) { + var size = (options && options.size) ? ' btn-'+options.size : ''; + return "<li>" + + "<div class='btn-group'>" + + "<a class='btn" + size + "' data-wysihtml5-action='change_view' title='" + locale.html.edit + "' tabindex='-1'><i class='icon-pencil'></i></a>" + + "</div>" + + "</li>"; + }, + + "color": function(locale, options) { + var size = (options && options.size) ? ' btn-'+options.size : ''; + return "<li class='dropdown'>" + + "<a class='btn dropdown-toggle" + size + "' data-toggle='dropdown' href='#' tabindex='-1'>" + + "<span class='current-color'>" + locale.colours.black + "</span> <b class='caret'></b>" + + "</a>" + + "<ul class='dropdown-menu'>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='black'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='black'>" + locale.colours.black + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='silver'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='silver'>" + locale.colours.silver + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='gray'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='gray'>" + locale.colours.gray + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='maroon'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='maroon'>" + locale.colours.maroon + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='red'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='red'>" + locale.colours.red + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='purple'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='purple'>" + locale.colours.purple + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='green'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='green'>" + locale.colours.green + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='olive'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='olive'>" + locale.colours.olive + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='navy'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='navy'>" + locale.colours.navy + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='blue'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='blue'>" + locale.colours.blue + "</a></li>" + + "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='orange'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='orange'>" + locale.colours.orange + "</a></li>" + + "</ul>" + + "</li>"; + } + }; + + var templates = function(key, locale, options) { + return tpl[key](locale, options); + }; + + + var Wysihtml5 = function(el, options) { + this.el = el; + var toolbarOpts = options || defaultOptions; + for(var t in toolbarOpts.customTemplates) { + tpl[t] = toolbarOpts.customTemplates[t]; + } + this.toolbar = this.createToolbar(el, toolbarOpts); + this.editor = this.createEditor(options); + + window.editor = this.editor; + + $('iframe.wysihtml5-sandbox').each(function(i, el){ + $(el.contentWindow).off('focus.wysihtml5').on({ + 'focus.wysihtml5' : function(){ + $('li.dropdown').removeClass('open'); + } + }); + }); + }; + + Wysihtml5.prototype = { + + constructor: Wysihtml5, + + createEditor: function(options) { + options = options || {}; + + // Add the toolbar to a clone of the options object so multiple instances + // of the WYISYWG don't break because "toolbar" is already defined + options = $.extend(true, {}, options); + options.toolbar = this.toolbar[0]; + + var editor = new wysi.Editor(this.el[0], options); + + if(options && options.events) { + for(var eventName in options.events) { + editor.on(eventName, options.events[eventName]); + } + } + return editor; + }, + + createToolbar: function(el, options) { + var self = this; + var toolbar = $("<ul/>", { + 'class' : "wysihtml5-toolbar", + 'style': "display:none" + }); + var culture = options.locale || defaultOptions.locale || "en"; + for(var key in defaultOptions) { + var value = false; + + if(options[key] !== undefined) { + if(options[key] === true) { + value = true; + } + } else { + value = defaultOptions[key]; + } + + if(value === true) { + toolbar.append(templates(key, locale[culture], options)); + + if(key === "html") { + this.initHtml(toolbar); + } + + if(key === "link") { + this.initInsertLink(toolbar); + } + + if(key === "image") { + this.initInsertImage(toolbar); + } + } + } + + if(options.toolbar) { + for(key in options.toolbar) { + toolbar.append(options.toolbar[key]); + } + } + + toolbar.find("a[data-wysihtml5-command='formatBlock']").click(function(e) { + var target = e.target || e.srcElement; + var el = $(target); + self.toolbar.find('.current-font').text(el.html()); + }); + + toolbar.find("a[data-wysihtml5-command='foreColor']").click(function(e) { + var target = e.target || e.srcElement; + var el = $(target); + self.toolbar.find('.current-color').text(el.html()); + }); + + this.el.before(toolbar); + + return toolbar; + }, + + initHtml: function(toolbar) { + var changeViewSelector = "a[data-wysihtml5-action='change_view']"; + toolbar.find(changeViewSelector).click(function(e) { + toolbar.find('a.btn').not(changeViewSelector).toggleClass('disabled'); + }); + }, + + initInsertImage: function(toolbar) { + var self = this; + var insertImageModal = toolbar.find('.bootstrap-wysihtml5-insert-image-modal'); + var urlInput = insertImageModal.find('.bootstrap-wysihtml5-insert-image-url'); + var insertButton = insertImageModal.find('a.btn-primary'); + var initialValue = urlInput.val(); + var caretBookmark; + + var insertImage = function() { + var url = urlInput.val(); + urlInput.val(initialValue); + self.editor.currentView.element.focus(); + if (caretBookmark) { + self.editor.composer.selection.setBookmark(caretBookmark); + caretBookmark = null; + } + self.editor.composer.commands.exec("insertImage", url); + }; + + urlInput.keypress(function(e) { + if(e.which == 13) { + insertImage(); + insertImageModal.modal('hide'); + } + }); + + insertButton.click(insertImage); + + insertImageModal.on('shown', function() { + urlInput.focus(); + }); + + insertImageModal.on('hide', function() { + self.editor.currentView.element.focus(); + }); + + toolbar.find('a[data-wysihtml5-command=insertImage]').click(function() { + var activeButton = $(this).hasClass("wysihtml5-command-active"); + + if (!activeButton) { + self.editor.currentView.element.focus(false); + caretBookmark = self.editor.composer.selection.getBookmark(); + insertImageModal.appendTo('body').modal('show'); + insertImageModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) { + e.stopPropagation(); + }); + return false; + } + else { + return true; + } + }); + }, + + initInsertLink: function(toolbar) { + var self = this; + var insertLinkModal = toolbar.find('.bootstrap-wysihtml5-insert-link-modal'); + var urlInput = insertLinkModal.find('.bootstrap-wysihtml5-insert-link-url'); + var insertButton = insertLinkModal.find('a.btn-primary'); + var initialValue = urlInput.val(); + var caretBookmark; + + var insertLink = function() { + var url = urlInput.val(); + urlInput.val(initialValue); + self.editor.currentView.element.focus(); + if (caretBookmark) { + self.editor.composer.selection.setBookmark(caretBookmark); + caretBookmark = null; + } + self.editor.composer.commands.exec("createLink", { + href: url, + target: "_blank", + rel: "nofollow" + }); + }; + var pressedEnter = false; + + urlInput.keypress(function(e) { + if(e.which == 13) { + insertLink(); + insertLinkModal.modal('hide'); + } + }); + + insertButton.click(insertLink); + + insertLinkModal.on('shown', function() { + urlInput.focus(); + }); + + insertLinkModal.on('hide', function() { + self.editor.currentView.element.focus(); + }); + + toolbar.find('a[data-wysihtml5-command=createLink]').click(function() { + var activeButton = $(this).hasClass("wysihtml5-command-active"); + + if (!activeButton) { + self.editor.currentView.element.focus(false); + caretBookmark = self.editor.composer.selection.getBookmark(); + insertLinkModal.appendTo('body').modal('show'); + insertLinkModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) { + e.stopPropagation(); + }); + return false; + } + else { + return true; + } + }); + } + }; + + // these define our public api + var methods = { + resetDefaults: function() { + $.fn.wysihtml5.defaultOptions = $.extend(true, {}, $.fn.wysihtml5.defaultOptionsCache); + }, + bypassDefaults: function(options) { + return this.each(function () { + var $this = $(this); + $this.data('wysihtml5', new Wysihtml5($this, options)); + }); + }, + shallowExtend: function (options) { + var settings = $.extend({}, $.fn.wysihtml5.defaultOptions, options || {}); + var that = this; + return methods.bypassDefaults.apply(that, [settings]); + }, + deepExtend: function(options) { + var settings = $.extend(true, {}, $.fn.wysihtml5.defaultOptions, options || {}); + var that = this; + return methods.bypassDefaults.apply(that, [settings]); + }, + init: function(options) { + var that = this; + return methods.shallowExtend.apply(that, [options]); + } + }; + + $.fn.wysihtml5 = function ( method ) { + if ( methods[method] ) { + return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); + } else if ( typeof method === 'object' || ! method ) { + return methods.init.apply( this, arguments ); + } else { + $.error( 'Method ' + method + ' does not exist on jQuery.wysihtml5' ); + } + }; + + $.fn.wysihtml5.Constructor = Wysihtml5; + + var defaultOptions = $.fn.wysihtml5.defaultOptions = { + "font-styles": true, + "color": false, + "emphasis": true, + "lists": true, + "html": false, + "link": true, + "image": true, + events: {}, + parserRules: { + classes: { + // (path_to_project/lib/css/wysiwyg-color.css) + "wysiwyg-color-silver" : 1, + "wysiwyg-color-gray" : 1, + "wysiwyg-color-white" : 1, + "wysiwyg-color-maroon" : 1, + "wysiwyg-color-red" : 1, + "wysiwyg-color-purple" : 1, + "wysiwyg-color-fuchsia" : 1, + "wysiwyg-color-green" : 1, + "wysiwyg-color-lime" : 1, + "wysiwyg-color-olive" : 1, + "wysiwyg-color-yellow" : 1, + "wysiwyg-color-navy" : 1, + "wysiwyg-color-blue" : 1, + "wysiwyg-color-teal" : 1, + "wysiwyg-color-aqua" : 1, + "wysiwyg-color-orange" : 1 + }, + tags: { + "b": {}, + "i": {}, + "br": {}, + "ol": {}, + "ul": {}, + "li": {}, + "h1": {}, + "h2": {}, + "h3": {}, + "blockquote": {}, + "u": 1, + "img": { + "check_attributes": { + "width": "numbers", + "alt": "alt", + "src": "url", + "height": "numbers" + } + }, + "a": { + set_attributes: { + target: "_blank", + rel: "nofollow" + }, + check_attributes: { + href: "url" // important to avoid XSS + } + }, + "span": 1, + "div": 1, + // to allow save and edit files with code tag hacks + "code": 1, + "pre": 1 + } + }, + stylesheets: ["./lib/css/wysiwyg-color.css"], // (path_to_project/lib/css/wysiwyg-color.css) + locale: "en" + }; + + if (typeof $.fn.wysihtml5.defaultOptionsCache === 'undefined') { + $.fn.wysihtml5.defaultOptionsCache = $.extend(true, {}, $.fn.wysihtml5.defaultOptions); + } + + var locale = $.fn.wysihtml5.locale = { + en: { + font_styles: { + normal: "Normal text", + h1: "Heading 1", + h2: "Heading 2", + h3: "Heading 3" + }, + emphasis: { + bold: "Bold", + italic: "Italic", + underline: "Underline" + }, + lists: { + unordered: "Unordered list", + ordered: "Ordered list", + outdent: "Outdent", + indent: "Indent" + }, + link: { + insert: "Insert link", + cancel: "Cancel" + }, + image: { + insert: "Insert image", + cancel: "Cancel" + }, + html: { + edit: "Edit HTML" + }, + colours: { + black: "Black", + silver: "Silver", + gray: "Grey", + maroon: "Maroon", + red: "Red", + purple: "Purple", + green: "Green", + olive: "Olive", + navy: "Navy", + blue: "Blue", + orange: "Orange" + } + } + }; + +}(window.jQuery, window.wysihtml5); diff --git a/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.min.js b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.min.js new file mode 100644 index 0000000..59d2961 --- /dev/null +++ b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.min.js @@ -0,0 +1 @@ +!function($,wysi){"use strict";var tpl={"font-styles":function(locale,options){var size=options&&options.size?" btn-"+options.size:"";return"<li class='dropdown'>"+"<a class='btn dropdown-toggle"+size+"' data-toggle='dropdown' href='#'>"+"<i class='icon-font'></i> <span class='current-font'>"+locale.font_styles.normal+"</span> <b class='caret'></b>"+"</a>"+"<ul class='dropdown-menu'>"+"<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='div' tabindex='-1'>"+locale.font_styles.normal+"</a></li>"+"<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h1' tabindex='-1'>"+locale.font_styles.h1+"</a></li>"+"<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h2' tabindex='-1'>"+locale.font_styles.h2+"</a></li>"+"<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h3' tabindex='-1'>"+locale.font_styles.h3+"</a></li>"+"</ul>"+"</li>"},emphasis:function(locale,options){var size=options&&options.size?" btn-"+options.size:"";return"<li>"+"<div class='btn-group'>"+"<a class='btn"+size+"' data-wysihtml5-command='bold' title='CTRL+B' tabindex='-1'>"+locale.emphasis.bold+"</a>"+"<a class='btn"+size+"' data-wysihtml5-command='italic' title='CTRL+I' tabindex='-1'>"+locale.emphasis.italic+"</a>"+"<a class='btn"+size+"' data-wysihtml5-command='underline' title='CTRL+U' tabindex='-1'>"+locale.emphasis.underline+"</a>"+"</div>"+"</li>"},lists:function(locale,options){var size=options&&options.size?" btn-"+options.size:"";return"<li>"+"<div class='btn-group'>"+"<a class='btn"+size+"' data-wysihtml5-command='insertUnorderedList' title='"+locale.lists.unordered+"' tabindex='-1'><i class='icon-list'></i></a>"+"<a class='btn"+size+"' data-wysihtml5-command='insertOrderedList' title='"+locale.lists.ordered+"' tabindex='-1'><i class='icon-th-list'></i></a>"+"<a class='btn"+size+"' data-wysihtml5-command='Outdent' title='"+locale.lists.outdent+"' tabindex='-1'><i class='icon-indent-right'></i></a>"+"<a class='btn"+size+"' data-wysihtml5-command='Indent' title='"+locale.lists.indent+"' tabindex='-1'><i class='icon-indent-left'></i></a>"+"</div>"+"</li>"},link:function(locale,options){var size=options&&options.size?" btn-"+options.size:"";return"<li>"+"<div class='bootstrap-wysihtml5-insert-link-modal modal hide fade'>"+"<div class='modal-header'>"+"<a class='close' data-dismiss='modal'>×</a>"+"<h3>"+locale.link.insert+"</h3>"+"</div>"+"<div class='modal-body'>"+"<input value='http://' class='bootstrap-wysihtml5-insert-link-url input-xlarge'>"+"</div>"+"<div class='modal-footer'>"+"<a href='#' class='btn' data-dismiss='modal'>"+locale.link.cancel+"</a>"+"<a href='#' class='btn btn-primary' data-dismiss='modal'>"+locale.link.insert+"</a>"+"</div>"+"</div>"+"<a class='btn"+size+"' data-wysihtml5-command='createLink' title='"+locale.link.insert+"' tabindex='-1'><i class='icon-share'></i></a>"+"</li>"},image:function(locale,options){var size=options&&options.size?" btn-"+options.size:"";return"<li>"+"<div class='bootstrap-wysihtml5-insert-image-modal modal hide fade'>"+"<div class='modal-header'>"+"<a class='close' data-dismiss='modal'>×</a>"+"<h3>"+locale.image.insert+"</h3>"+"</div>"+"<div class='modal-body'>"+"<input value='http://' class='bootstrap-wysihtml5-insert-image-url input-xlarge'>"+"</div>"+"<div class='modal-footer'>"+"<a href='#' class='btn' data-dismiss='modal'>"+locale.image.cancel+"</a>"+"<a href='#' class='btn btn-primary' data-dismiss='modal'>"+locale.image.insert+"</a>"+"</div>"+"</div>"+"<a class='btn"+size+"' data-wysihtml5-command='insertImage' title='"+locale.image.insert+"' tabindex='-1'><i class='icon-picture'></i></a>"+"</li>"},html:function(locale,options){var size=options&&options.size?" btn-"+options.size:"";return"<li>"+"<div class='btn-group'>"+"<a class='btn"+size+"' data-wysihtml5-action='change_view' title='"+locale.html.edit+"' tabindex='-1'><i class='icon-pencil'></i></a>"+"</div>"+"</li>"},color:function(locale,options){var size=options&&options.size?" btn-"+options.size:"";return"<li class='dropdown'>"+"<a class='btn dropdown-toggle"+size+"' data-toggle='dropdown' href='#' tabindex='-1'>"+"<span class='current-color'>"+locale.colours.black+"</span> <b class='caret'></b>"+"</a>"+"<ul class='dropdown-menu'>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='black'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='black'>"+locale.colours.black+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='silver'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='silver'>"+locale.colours.silver+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='gray'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='gray'>"+locale.colours.gray+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='maroon'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='maroon'>"+locale.colours.maroon+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='red'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='red'>"+locale.colours.red+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='purple'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='purple'>"+locale.colours.purple+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='green'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='green'>"+locale.colours.green+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='olive'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='olive'>"+locale.colours.olive+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='navy'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='navy'>"+locale.colours.navy+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='blue'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='blue'>"+locale.colours.blue+"</a></li>"+"<li><div class='wysihtml5-colors' data-wysihtml5-command-value='orange'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='orange'>"+locale.colours.orange+"</a></li>"+"</ul>"+"</li>"}};var templates=function(key,locale,options){return tpl[key](locale,options)};var Wysihtml5=function(el,options){this.el=el;var toolbarOpts=options||defaultOptions;for(var t in toolbarOpts.customTemplates){tpl[t]=toolbarOpts.customTemplates[t]}this.toolbar=this.createToolbar(el,toolbarOpts);this.editor=this.createEditor(options);window.editor=this.editor;$("iframe.wysihtml5-sandbox").each(function(i,el){$(el.contentWindow).off("focus.wysihtml5").on({"focus.wysihtml5":function(){$("li.dropdown").removeClass("open")}})})};Wysihtml5.prototype={constructor:Wysihtml5,createEditor:function(options){options=options||{};options=$.extend(true,{},options);options.toolbar=this.toolbar[0];var editor=new wysi.Editor(this.el[0],options);if(options&&options.events){for(var eventName in options.events){editor.on(eventName,options.events[eventName])}}return editor},createToolbar:function(el,options){var self=this;var toolbar=$("<ul/>",{"class":"wysihtml5-toolbar",style:"display:none"});var culture=options.locale||defaultOptions.locale||"en";for(var key in defaultOptions){var value=false;if(options[key]!==undefined){if(options[key]===true){value=true}}else{value=defaultOptions[key]}if(value===true){toolbar.append(templates(key,locale[culture],options));if(key==="html"){this.initHtml(toolbar)}if(key==="link"){this.initInsertLink(toolbar)}if(key==="image"){this.initInsertImage(toolbar)}}}if(options.toolbar){for(key in options.toolbar){toolbar.append(options.toolbar[key])}}toolbar.find("a[data-wysihtml5-command='formatBlock']").click(function(e){var target=e.target||e.srcElement;var el=$(target);self.toolbar.find(".current-font").text(el.html())});toolbar.find("a[data-wysihtml5-command='foreColor']").click(function(e){var target=e.target||e.srcElement;var el=$(target);self.toolbar.find(".current-color").text(el.html())});this.el.before(toolbar);return toolbar},initHtml:function(toolbar){var changeViewSelector="a[data-wysihtml5-action='change_view']";toolbar.find(changeViewSelector).click(function(e){toolbar.find("a.btn").not(changeViewSelector).toggleClass("disabled")})},initInsertImage:function(toolbar){var self=this;var insertImageModal=toolbar.find(".bootstrap-wysihtml5-insert-image-modal");var urlInput=insertImageModal.find(".bootstrap-wysihtml5-insert-image-url");var insertButton=insertImageModal.find("a.btn-primary");var initialValue=urlInput.val();var caretBookmark;var insertImage=function(){var url=urlInput.val();urlInput.val(initialValue);self.editor.currentView.element.focus();if(caretBookmark){self.editor.composer.selection.setBookmark(caretBookmark);caretBookmark=null}self.editor.composer.commands.exec("insertImage",url)};urlInput.keypress(function(e){if(e.which==13){insertImage();insertImageModal.modal("hide")}});insertButton.click(insertImage);insertImageModal.on("shown",function(){urlInput.focus()});insertImageModal.on("hide",function(){self.editor.currentView.element.focus()});toolbar.find("a[data-wysihtml5-command=insertImage]").click(function(){var activeButton=$(this).hasClass("wysihtml5-command-active");if(!activeButton){self.editor.currentView.element.focus(false);caretBookmark=self.editor.composer.selection.getBookmark();insertImageModal.appendTo("body").modal("show");insertImageModal.on("click.dismiss.modal",'[data-dismiss="modal"]',function(e){e.stopPropagation()});return false}else{return true}})},initInsertLink:function(toolbar){var self=this;var insertLinkModal=toolbar.find(".bootstrap-wysihtml5-insert-link-modal");var urlInput=insertLinkModal.find(".bootstrap-wysihtml5-insert-link-url");var insertButton=insertLinkModal.find("a.btn-primary");var initialValue=urlInput.val();var caretBookmark;var insertLink=function(){var url=urlInput.val();urlInput.val(initialValue);self.editor.currentView.element.focus();if(caretBookmark){self.editor.composer.selection.setBookmark(caretBookmark);caretBookmark=null}self.editor.composer.commands.exec("createLink",{href:url,target:"_blank",rel:"nofollow"})};var pressedEnter=false;urlInput.keypress(function(e){if(e.which==13){insertLink();insertLinkModal.modal("hide")}});insertButton.click(insertLink);insertLinkModal.on("shown",function(){urlInput.focus()});insertLinkModal.on("hide",function(){self.editor.currentView.element.focus()});toolbar.find("a[data-wysihtml5-command=createLink]").click(function(){var activeButton=$(this).hasClass("wysihtml5-command-active");if(!activeButton){self.editor.currentView.element.focus(false);caretBookmark=self.editor.composer.selection.getBookmark();insertLinkModal.appendTo("body").modal("show");insertLinkModal.on("click.dismiss.modal",'[data-dismiss="modal"]',function(e){e.stopPropagation()});return false}else{return true}})}};var methods={resetDefaults:function(){$.fn.wysihtml5.defaultOptions=$.extend(true,{},$.fn.wysihtml5.defaultOptionsCache)},bypassDefaults:function(options){return this.each(function(){var $this=$(this);$this.data("wysihtml5",new Wysihtml5($this,options))})},shallowExtend:function(options){var settings=$.extend({},$.fn.wysihtml5.defaultOptions,options||{});var that=this;return methods.bypassDefaults.apply(that,[settings])},deepExtend:function(options){var settings=$.extend(true,{},$.fn.wysihtml5.defaultOptions,options||{});var that=this;return methods.bypassDefaults.apply(that,[settings])},init:function(options){var that=this;return methods.shallowExtend.apply(that,[options])}};$.fn.wysihtml5=function(method){if(methods[method]){return methods[method].apply(this,Array.prototype.slice.call(arguments,1))}else if(typeof method==="object"||!method){return methods.init.apply(this,arguments)}else{$.error("Method "+method+" does not exist on jQuery.wysihtml5")}};$.fn.wysihtml5.Constructor=Wysihtml5;var defaultOptions=$.fn.wysihtml5.defaultOptions={"font-styles":true,color:false,emphasis:true,lists:true,html:false,link:true,image:true,events:{},parserRules:{classes:{"wysiwyg-color-silver":1,"wysiwyg-color-gray":1,"wysiwyg-color-white":1,"wysiwyg-color-maroon":1,"wysiwyg-color-red":1,"wysiwyg-color-purple":1,"wysiwyg-color-fuchsia":1,"wysiwyg-color-green":1,"wysiwyg-color-lime":1,"wysiwyg-color-olive":1,"wysiwyg-color-yellow":1,"wysiwyg-color-navy":1,"wysiwyg-color-blue":1,"wysiwyg-color-teal":1,"wysiwyg-color-aqua":1,"wysiwyg-color-orange":1},tags:{b:{},i:{},br:{},ol:{},ul:{},li:{},h1:{},h2:{},h3:{},blockquote:{},u:1,img:{check_attributes:{width:"numbers",alt:"alt",src:"url",height:"numbers"}},a:{set_attributes:{target:"_blank",rel:"nofollow"},check_attributes:{href:"url"}},span:1,div:1,code:1,pre:1}},stylesheets:["./lib/css/wysiwyg-color.css"],locale:"en"};if(typeof $.fn.wysihtml5.defaultOptionsCache==="undefined"){$.fn.wysihtml5.defaultOptionsCache=$.extend(true,{},$.fn.wysihtml5.defaultOptions)}var locale=$.fn.wysihtml5.locale={en:{font_styles:{normal:"Normal text",h1:"Heading 1",h2:"Heading 2",h3:"Heading 3"},emphasis:{bold:"Bold",italic:"Italic",underline:"Underline"},lists:{unordered:"Unordered list",ordered:"Ordered list",outdent:"Outdent",indent:"Indent"},link:{insert:"Insert link",cancel:"Cancel"},image:{insert:"Insert image",cancel:"Cancel"},html:{edit:"Edit HTML"},colours:{black:"Black",silver:"Silver",gray:"Grey",maroon:"Maroon",red:"Red",purple:"Purple",green:"Green",olive:"Olive",navy:"Navy",blue:"Blue",orange:"Orange"}}}}(window.jQuery,window.wysihtml5); \ No newline at end of file diff --git a/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.js b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.js new file mode 100644 index 0000000..939dca9 --- /dev/null +++ b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.js @@ -0,0 +1,9521 @@ +/** + * @license wysihtml5 v0.3.0 + * https://github.com/xing/wysihtml5 + * + * Author: Christopher Blum (https://github.com/tiff) + * + * Copyright (C) 2012 XING AG + * Licensed under the MIT license (MIT) + * + */ +var wysihtml5 = { + version: "0.3.0", + + // namespaces + commands: {}, + dom: {}, + quirks: {}, + toolbar: {}, + lang: {}, + selection: {}, + views: {}, + + INVISIBLE_SPACE: "\uFEFF", + + EMPTY_FUNCTION: function() {}, + + ELEMENT_NODE: 1, + TEXT_NODE: 3, + + BACKSPACE_KEY: 8, + ENTER_KEY: 13, + ESCAPE_KEY: 27, + SPACE_KEY: 32, + DELETE_KEY: 46 +};/** + * @license Rangy, a cross-browser JavaScript range and selection library + * http://code.google.com/p/rangy/ + * + * Copyright 2011, Tim Down + * Licensed under the MIT license. + * Version: 1.2.2 + * Build date: 13 November 2011 + */ +window['rangy'] = (function() { + + + var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; + + var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"]; + + var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", + "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", + "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; + + var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; + + // Subset of TextRange's full set of methods that we're interested in + var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark", + "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"]; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Trio of functions taken from Peter Michaux's article: + // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting + function isHostMethod(o, p) { + var t = typeof o[p]; + return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; + } + + function isHostObject(o, p) { + return !!(typeof o[p] == OBJECT && o[p]); + } + + function isHostProperty(o, p) { + return typeof o[p] != UNDEFINED; + } + + // Creates a convenience function to save verbose repeated calls to tests functions + function createMultiplePropertyTest(testFunc) { + return function(o, props) { + var i = props.length; + while (i--) { + if (!testFunc(o, props[i])) { + return false; + } + } + return true; + }; + } + + // Next trio of functions are a convenience to save verbose repeated calls to previous two functions + var areHostMethods = createMultiplePropertyTest(isHostMethod); + var areHostObjects = createMultiplePropertyTest(isHostObject); + var areHostProperties = createMultiplePropertyTest(isHostProperty); + + function isTextRange(range) { + return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); + } + + var api = { + version: "1.2.2", + initialized: false, + supported: true, + + util: { + isHostMethod: isHostMethod, + isHostObject: isHostObject, + isHostProperty: isHostProperty, + areHostMethods: areHostMethods, + areHostObjects: areHostObjects, + areHostProperties: areHostProperties, + isTextRange: isTextRange + }, + + features: {}, + + modules: {}, + config: { + alertOnWarn: false, + preferTextRange: false + } + }; + + function fail(reason) { + window.alert("Rangy not supported in your browser. Reason: " + reason); + api.initialized = true; + api.supported = false; + } + + api.fail = fail; + + function warn(msg) { + var warningMessage = "Rangy warning: " + msg; + if (api.config.alertOnWarn) { + window.alert(warningMessage); + } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) { + window.console.log(warningMessage); + } + } + + api.warn = warn; + + if ({}.hasOwnProperty) { + api.util.extend = function(o, props) { + for (var i in props) { + if (props.hasOwnProperty(i)) { + o[i] = props[i]; + } + } + }; + } else { + fail("hasOwnProperty not supported"); + } + + var initListeners = []; + var moduleInitializers = []; + + // Initialization + function init() { + if (api.initialized) { + return; + } + var testRange; + var implementsDomRange = false, implementsTextRange = false; + + // First, perform basic feature tests + + if (isHostMethod(document, "createRange")) { + testRange = document.createRange(); + if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { + implementsDomRange = true; + } + testRange.detach(); + } + + var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0]; + + if (body && isHostMethod(body, "createTextRange")) { + testRange = body.createTextRange(); + if (isTextRange(testRange)) { + implementsTextRange = true; + } + } + + if (!implementsDomRange && !implementsTextRange) { + fail("Neither Range nor TextRange are implemented"); + } + + api.initialized = true; + api.features = { + implementsDomRange: implementsDomRange, + implementsTextRange: implementsTextRange + }; + + // Initialize modules and call init listeners + var allListeners = moduleInitializers.concat(initListeners); + for (var i = 0, len = allListeners.length; i < len; ++i) { + try { + allListeners[i](api); + } catch (ex) { + if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { + window.console.log("Init listener threw an exception. Continuing.", ex); + } + + } + } + } + + // Allow external scripts to initialize this library in case it's loaded after the document has loaded + api.init = init; + + // Execute listener immediately if already initialized + api.addInitListener = function(listener) { + if (api.initialized) { + listener(api); + } else { + initListeners.push(listener); + } + }; + + var createMissingNativeApiListeners = []; + + api.addCreateMissingNativeApiListener = function(listener) { + createMissingNativeApiListeners.push(listener); + }; + + function createMissingNativeApi(win) { + win = win || window; + init(); + + // Notify listeners + for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) { + createMissingNativeApiListeners[i](win); + } + } + + api.createMissingNativeApi = createMissingNativeApi; + + /** + * @constructor + */ + function Module(name) { + this.name = name; + this.initialized = false; + this.supported = false; + } + + Module.prototype.fail = function(reason) { + this.initialized = true; + this.supported = false; + + throw new Error("Module '" + this.name + "' failed to load: " + reason); + }; + + Module.prototype.warn = function(msg) { + api.warn("Module " + this.name + ": " + msg); + }; + + Module.prototype.createError = function(msg) { + return new Error("Error in Rangy " + this.name + " module: " + msg); + }; + + api.createModule = function(name, initFunc) { + var module = new Module(name); + api.modules[name] = module; + + moduleInitializers.push(function(api) { + initFunc(api, module); + module.initialized = true; + module.supported = true; + }); + }; + + api.requireModules = function(modules) { + for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) { + moduleName = modules[i]; + module = api.modules[moduleName]; + if (!module || !(module instanceof Module)) { + throw new Error("Module '" + moduleName + "' not found"); + } + if (!module.supported) { + throw new Error("Module '" + moduleName + "' not supported"); + } + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Wait for document to load before running tests + + var docReady = false; + + var loadHandler = function(e) { + + if (!docReady) { + docReady = true; + if (!api.initialized) { + init(); + } + } + }; + + // Test whether we have window and document objects that we will need + if (typeof window == UNDEFINED) { + fail("No window found"); + return; + } + if (typeof document == UNDEFINED) { + fail("No document found"); + return; + } + + if (isHostMethod(document, "addEventListener")) { + document.addEventListener("DOMContentLoaded", loadHandler, false); + } + + // Add a fallback in case the DOMContentLoaded event isn't supported + if (isHostMethod(window, "addEventListener")) { + window.addEventListener("load", loadHandler, false); + } else if (isHostMethod(window, "attachEvent")) { + window.attachEvent("onload", loadHandler); + } else { + fail("Window does not have required addEventListener or attachEvent method"); + } + + return api; +})(); +rangy.createModule("DomUtil", function(api, module) { + + var UNDEF = "undefined"; + var util = api.util; + + // Perform feature tests + if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { + module.fail("document missing a Node creation method"); + } + + if (!util.isHostMethod(document, "getElementsByTagName")) { + module.fail("document missing getElementsByTagName method"); + } + + var el = document.createElement("div"); + if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { + module.fail("Incomplete Element implementation"); + } + + // innerHTML is required for Range's createContextualFragment method + if (!util.isHostProperty(el, "innerHTML")) { + module.fail("Element is missing innerHTML property"); + } + + var textNode = document.createTextNode("test"); + if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || + !util.areHostProperties(textNode, ["data"]))) { + module.fail("Incomplete Text Node implementation"); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been + // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that + // contains just the document as a single element and the value searched for is the document. + var arrayContains = /*Array.prototype.indexOf ? + function(arr, val) { + return arr.indexOf(val) > -1; + }:*/ + + function(arr, val) { + var i = arr.length; + while (i--) { + if (arr[i] === val) { + return true; + } + } + return false; + }; + + // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI + function isHtmlNamespace(node) { + var ns; + return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); + } + + function parentElement(node) { + var parent = node.parentNode; + return (parent.nodeType == 1) ? parent : null; + } + + function getNodeIndex(node) { + var i = 0; + while( (node = node.previousSibling) ) { + i++; + } + return i; + } + + function getNodeLength(node) { + var childNodes; + return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0); + } + + function getCommonAncestor(node1, node2) { + var ancestors = [], n; + for (n = node1; n; n = n.parentNode) { + ancestors.push(n); + } + + for (n = node2; n; n = n.parentNode) { + if (arrayContains(ancestors, n)) { + return n; + } + } + + return null; + } + + function isAncestorOf(ancestor, descendant, selfIsAncestor) { + var n = selfIsAncestor ? descendant : descendant.parentNode; + while (n) { + if (n === ancestor) { + return true; + } else { + n = n.parentNode; + } + } + return false; + } + + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { + var p, n = selfIsAncestor ? node : node.parentNode; + while (n) { + p = n.parentNode; + if (p === ancestor) { + return n; + } + n = p; + } + return null; + } + + function isCharacterDataNode(node) { + var t = node.nodeType; + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment + } + + function insertAfter(node, precedingNode) { + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; + if (nextNode) { + parent.insertBefore(node, nextNode); + } else { + parent.appendChild(node); + } + return node; + } + + // Note that we cannot use splitText() because it is bugridden in IE 9. + function splitDataNode(node, index) { + var newNode = node.cloneNode(false); + newNode.deleteData(0, index); + node.deleteData(index, node.length - index); + insertAfter(newNode, node); + return newNode; + } + + function getDocument(node) { + if (node.nodeType == 9) { + return node; + } else if (typeof node.ownerDocument != UNDEF) { + return node.ownerDocument; + } else if (typeof node.document != UNDEF) { + return node.document; + } else if (node.parentNode) { + return getDocument(node.parentNode); + } else { + throw new Error("getDocument: no document found for node"); + } + } + + function getWindow(node) { + var doc = getDocument(node); + if (typeof doc.defaultView != UNDEF) { + return doc.defaultView; + } else if (typeof doc.parentWindow != UNDEF) { + return doc.parentWindow; + } else { + throw new Error("Cannot get a window object for node"); + } + } + + function getIframeDocument(iframeEl) { + if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument; + } else if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow.document; + } else { + throw new Error("getIframeWindow: No Document object found for iframe element"); + } + } + + function getIframeWindow(iframeEl) { + if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow; + } else if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument.defaultView; + } else { + throw new Error("getIframeWindow: No Window object found for iframe element"); + } + } + + function getBody(doc) { + return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; + } + + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; + } + return node; + } + + function comparePoints(nodeA, offsetA, nodeB, offsetB) { + // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing + var nodeC, root, childA, childB, n; + if (nodeA == nodeB) { + + // Case 1: nodes are the same + return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { + + // Case 2: node C (container B or an ancestor) is a child node of A + return offsetA <= getNodeIndex(nodeC) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { + + // Case 3: node C (container A or an ancestor) is a child node of B + return getNodeIndex(nodeC) < offsetB ? -1 : 1; + } else { + + // Case 4: containers are siblings or descendants of siblings + root = getCommonAncestor(nodeA, nodeB); + childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); + childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); + + if (childA === childB) { + // This shouldn't be possible + + throw new Error("comparePoints got to case 4 and childA and childB are the same!"); + } else { + n = root.firstChild; + while (n) { + if (n === childA) { + return -1; + } else if (n === childB) { + return 1; + } + n = n.nextSibling; + } + throw new Error("Should not be here!"); + } + } + } + + function fragmentFromNodeChildren(node) { + var fragment = getDocument(node).createDocumentFragment(), child; + while ( (child = node.firstChild) ) { + fragment.appendChild(child); + } + return fragment; + } + + function inspectNode(node) { + if (!node) { + return "[No node]"; + } + if (isCharacterDataNode(node)) { + return '"' + node.data + '"'; + } else if (node.nodeType == 1) { + var idAttr = node.id ? ' id="' + node.id + '"' : ""; + return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]"; + } else { + return node.nodeName; + } + } + + /** + * @constructor + */ + function NodeIterator(root) { + this.root = root; + this._next = root; + } + + NodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + var n = this._current = this._next; + var child, next; + if (this._current) { + child = n.firstChild; + if (child) { + this._next = child; + } else { + next = null; + while ((n !== this.root) && !(next = n.nextSibling)) { + n = n.parentNode; + } + this._next = next; + } + } + return this._current; + }, + + detach: function() { + this._current = this._next = this.root = null; + } + }; + + function createIterator(root) { + return new NodeIterator(root); + } + + /** + * @constructor + */ + function DomPosition(node, offset) { + this.node = node; + this.offset = offset; + } + + DomPosition.prototype = { + equals: function(pos) { + return this.node === pos.node & this.offset == pos.offset; + }, + + inspect: function() { + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; + } + }; + + /** + * @constructor + */ + function DOMException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "DOMException: " + this.codeName; + } + + DOMException.prototype = { + INDEX_SIZE_ERR: 1, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INVALID_STATE_ERR: 11 + }; + + DOMException.prototype.toString = function() { + return this.message; + }; + + api.dom = { + arrayContains: arrayContains, + isHtmlNamespace: isHtmlNamespace, + parentElement: parentElement, + getNodeIndex: getNodeIndex, + getNodeLength: getNodeLength, + getCommonAncestor: getCommonAncestor, + isAncestorOf: isAncestorOf, + getClosestAncestorIn: getClosestAncestorIn, + isCharacterDataNode: isCharacterDataNode, + insertAfter: insertAfter, + splitDataNode: splitDataNode, + getDocument: getDocument, + getWindow: getWindow, + getIframeWindow: getIframeWindow, + getIframeDocument: getIframeDocument, + getBody: getBody, + getRootContainer: getRootContainer, + comparePoints: comparePoints, + inspectNode: inspectNode, + fragmentFromNodeChildren: fragmentFromNodeChildren, + createIterator: createIterator, + DomPosition: DomPosition + }; + + api.DOMException = DOMException; +});rangy.createModule("DomRange", function(api, module) { + api.requireModules( ["DomUtil"] ); + + + var dom = api.dom; + var DomPosition = dom.DomPosition; + var DOMException = api.DOMException; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Utility functions + + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true)); + } + + function getRangeDocument(range) { + return dom.getDocument(range.startContainer); + } + + function dispatchEvent(range, type, args) { + var listeners = range._listeners[type]; + if (listeners) { + for (var i = 0, len = listeners.length; i < len; ++i) { + listeners[i].call(range, {target: range, args: args}); + } + } + } + + function getBoundaryBeforeNode(node) { + return new DomPosition(node.parentNode, dom.getNodeIndex(node)); + } + + function getBoundaryAfterNode(node) { + return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1); + } + + function insertNodeAtPosition(node, n, o) { + var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; + if (dom.isCharacterDataNode(n)) { + if (o == n.length) { + dom.insertAfter(node, n); + } else { + n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o)); + } + } else if (o >= n.childNodes.length) { + n.appendChild(node); + } else { + n.insertBefore(node, n.childNodes[o]); + } + return firstNodeInserted; + } + + function cloneSubtree(iterator) { + var partiallySelected; + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + partiallySelected = iterator.isPartiallySelectedSubtree(); + + node = node.cloneNode(!partiallySelected); + if (partiallySelected) { + subIterator = iterator.getSubtreeIterator(); + node.appendChild(cloneSubtree(subIterator)); + subIterator.detach(true); + } + + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function iterateSubtree(rangeIterator, func, iteratorState) { + var it, n; + iteratorState = iteratorState || { stop: false }; + for (var node, subRangeIterator; node = rangeIterator.next(); ) { + //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node)); + if (rangeIterator.isPartiallySelectedSubtree()) { + // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the + // node selected by the Range. + if (func(node) === false) { + iteratorState.stop = true; + return; + } else { + subRangeIterator = rangeIterator.getSubtreeIterator(); + iterateSubtree(subRangeIterator, func, iteratorState); + subRangeIterator.detach(true); + if (iteratorState.stop) { + return; + } + } + } else { + // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its + // descendant + it = dom.createIterator(node); + while ( (n = it.next()) ) { + if (func(n) === false) { + iteratorState.stop = true; + return; + } + } + } + } + } + + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(true); + } else { + iterator.remove(); + } + } + } + + function extractSubtree(iterator) { + + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + + + if (iterator.isPartiallySelectedSubtree()) { + node = node.cloneNode(false); + subIterator = iterator.getSubtreeIterator(); + node.appendChild(extractSubtree(subIterator)); + subIterator.detach(true); + } else { + iterator.remove(); + } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function getNodesInRange(range, nodeTypes, filter) { + //log.info("getNodesInRange, " + nodeTypes.join(",")); + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); + } + + var nodes = []; + iterateSubtree(new RangeIterator(range, false), function(node) { + if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) { + nodes.push(node); + } + }); + return nodes; + } + + function inspect(range) { + var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); + return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + + dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) + + /** + * @constructor + */ + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + + + + if (!range.collapsed) { + this.sc = range.startContainer; + this.so = range.startOffset; + this.ec = range.endContainer; + this.eo = range.endOffset; + var root = range.commonAncestorContainer; + + if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) { + this.isSingleCharacterDataNode = true; + this._first = this._last = this._next = this.sc; + } else { + this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ? + this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true); + this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ? + this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true); + } + + } + } + + RangeIterator.prototype = { + _current: null, + _next: null, + _first: null, + _last: null, + isSingleCharacterDataNode: false, + + reset: function() { + this._current = null; + this._next = this._first; + }, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; + + // Check for partially selected text nodes + if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { + if (current === this.ec) { + + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); + } + if (this._current === this.sc) { + + (current = current.cloneNode(true)).deleteData(0, this.so); + } + } + } + + return current; + }, + + remove: function() { + var current = this._current, start, end; + + if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { + start = (current === this.sc) ? this.so : 0; + end = (current === this.ec) ? this.eo : current.length; + if (start != end) { + current.deleteData(start, end - start); + } + } else { + if (current.parentNode) { + current.parentNode.removeChild(current); + } else { + + } + } + }, + + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); + }, + + getSubtreeIterator: function() { + var subRange; + if (this.isSingleCharacterDataNode) { + subRange = this.range.cloneRange(); + subRange.collapse(); + } else { + subRange = new Range(getRangeDocument(this.range)); + var current = this._current; + var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current); + + if (dom.isAncestorOf(current, this.sc, true)) { + startContainer = this.sc; + startOffset = this.so; + } + if (dom.isAncestorOf(current, this.ec, true)) { + endContainer = this.ec; + endOffset = this.eo; + } + + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); + } + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); + }, + + detach: function(detachRange) { + if (detachRange) { + this.range.detach(); + } + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Exceptions + + /** + * @constructor + */ + function RangeException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "RangeException: " + this.codeName; + } + + RangeException.prototype = { + BAD_BOUNDARYPOINTS_ERR: 1, + INVALID_NODE_TYPE_ERR: 2 + }; + + RangeException.prototype.toString = function() { + return this.message; + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + /** + * Currently iterates through all nodes in the range on creation until I think of a decent way to do it + * TODO: Look into making this a proper iterator, not requiring preloading everything first + * @constructor + */ + function RangeNodeIterator(range, nodeTypes, filter) { + this.nodes = getNodesInRange(range, nodeTypes, filter); + this._next = this.nodes[0]; + this._position = 0; + } + + RangeNodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + this._current = this._next; + this._next = this.nodes[ ++this._position ]; + return this._current; + }, + + detach: function() { + this._current = this._next = this.nodes = null; + } + }; + + var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; + var rootContainerNodeTypes = [2, 9, 11]; + var readonlyNodeTypes = [5, 6, 10, 12]; + var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; + var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; + + function createAncestorFinder(nodeTypes) { + return function(node, selfIsAncestor) { + var t, n = selfIsAncestor ? node : node.parentNode; + while (n) { + t = n.nodeType; + if (dom.arrayContains(nodeTypes, t)) { + return n; + } + n = n.parentNode; + } + return null; + }; + } + + var getRootContainer = dom.getRootContainer; + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); + + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { + throw new RangeException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertNotDetached(range) { + if (!range.startContainer) { + throw new DOMException("INVALID_STATE_ERR"); + } + } + + function assertValidNodeType(node, invalidTypes) { + if (!dom.arrayContains(invalidTypes, node.nodeType)) { + throw new RangeException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidOffset(node, offset) { + if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) { + throw new DOMException("INDEX_SIZE_ERR"); + } + } + + function assertSameDocumentOrFragment(node1, node2) { + if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } + + function assertNodeNotReadOnly(node) { + if (getReadonlyAncestor(node, true)) { + throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); + } + } + + function assertNode(node, codeName) { + if (!node) { + throw new DOMException(codeName); + } + } + + function isOrphan(node) { + return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); + } + + function isValidOffset(node, offset) { + return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length); + } + + function assertRangeValid(range) { + assertNotDetached(range); + if (isOrphan(range.startContainer) || isOrphan(range.endContainer) || + !isValidOffset(range.startContainer, range.startOffset) || + !isValidOffset(range.endContainer, range.endOffset)) { + throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test the browser's innerHTML support to decide how to implement createContextualFragment + var styleEl = document.createElement("style"); + var htmlParsingConforms = false; + try { + styleEl.innerHTML = "<b>x</b>"; + htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node + } catch (e) { + // IE 6 and 7 throw + } + + api.features.htmlParsingConforms = htmlParsingConforms; + + var createContextualFragment = htmlParsingConforms ? + + // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See + // discussion and base code for this implementation at issue 67. + // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface + // Thanks to Aleks Williams. + function(fragmentStr) { + // "Let node the context object's start's node." + var node = this.startContainer; + var doc = dom.getDocument(node); + + // "If the context object's start's node is null, raise an INVALID_STATE_ERR + // exception and abort these steps." + if (!node) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; + + // "Element: node" + if (node.nodeType == 1) { + el = node; + + // "Text, Comment: node's parentElement" + } else if (dom.isCharacterDataNode(node)) { + el = dom.parentElement(node); + } + + // "If either element is null or element's ownerDocument is an HTML document + // and element's local name is "html" and element's namespace is the HTML + // namespace" + if (el === null || ( + el.nodeName == "HTML" + && dom.isHtmlNamespace(dom.getDocument(el).documentElement) + && dom.isHtmlNamespace(el) + )) { + + // "let element be a new Element with "body" as its local name and the HTML + // namespace as its namespace."" + el = doc.createElement("body"); + } else { + el = el.cloneNode(false); + } + + // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." + // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." + // "In either case, the algorithm must be invoked with fragment as the input + // and element as the context element." + el.innerHTML = fragmentStr; + + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." + + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : + + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that + // previous versions of Rangy used (with the exception of using a body element rather than a div) + function(fragmentStr) { + assertNotDetached(this); + var doc = getRangeDocument(this); + var el = doc.createElement("body"); + el.innerHTML = fragmentStr; + + return dom.fragmentFromNodeChildren(el); + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + var s2s = 0, s2e = 1, e2e = 2, e2s = 3; + var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; + + function RangePrototype() {} + + RangePrototype.prototype = { + attachListener: function(type, listener) { + this._listeners[type].push(listener); + }, + + compareBoundaryPoints: function(how, range) { + assertRangeValid(this); + assertSameDocumentOrFragment(this.startContainer, range.startContainer); + + var nodeA, offsetA, nodeB, offsetB; + var prefixA = (how == e2s || how == s2s) ? "start" : "end"; + var prefixB = (how == s2e || how == s2s) ? "start" : "end"; + nodeA = this[prefixA + "Container"]; + offsetA = this[prefixA + "Offset"]; + nodeB = range[prefixB + "Container"]; + offsetB = range[prefixB + "Offset"]; + return dom.comparePoints(nodeA, offsetA, nodeB, offsetB); + }, + + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); + + if (dom.isAncestorOf(node, this.startContainer, true)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + + // No check for whether the container of the start of the Range is of a type that does not allow + // children of the type of node: the browser's DOM implementation should do this for us when we attempt + // to add the node + + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); + this.setStartBefore(firstNodeInserted); + }, + + cloneContents: function() { + assertRangeValid(this); + + var clone, frag; + if (this.collapsed) { + return getRangeDocument(this).createDocumentFragment(); + } else { + if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) { + clone = this.startContainer.cloneNode(true); + clone.data = clone.data.slice(this.startOffset, this.endOffset); + frag = getRangeDocument(this).createDocumentFragment(); + frag.appendChild(clone); + return frag; + } else { + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); + iterator.detach(); + } + return clone; + } + }, + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); + + if (!this.canSurroundContents()) { + throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); + } + + // Extract the contents + var content = this.extractContents(); + + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); + } + } + + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); + + this.selectNode(node); + }, + + cloneRange: function() { + assertRangeValid(this); + var range = new Range(getRangeDocument(this)); + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = this[prop]; + } + return range; + }, + + toString: function() { + assertRangeValid(this); + var sc = this.startContainer; + if (sc === this.endContainer && dom.isCharacterDataNode(sc)) { + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; + } else { + var textBits = [], iterator = new RangeIterator(this, true); + + iterateSubtree(iterator, function(node) { + // Accept only text or CDATA nodes, not comments + + if (node.nodeType == 3 || node.nodeType == 4) { + textBits.push(node.data); + } + }); + iterator.detach(); + return textBits.join(""); + } + }, + + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. + + compareNode: function(node) { + assertRangeValid(this); + + var parent = node.parentNode; + var nodeIndex = dom.getNodeIndex(node); + + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } + + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); + + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, + + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { + return -1; + } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { + return 1; + } + return 0; + }, + + createContextualFragment: createContextualFragment, + + toHtml: function() { + assertRangeValid(this); + var container = getRangeDocument(this).createElement("div"); + container.appendChild(this.cloneContents()); + return container.innerHTML; + }, + + // touchingIsIntersecting determines whether this method considers a node that borders a range intersects + // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) + intersectsNode: function(node, touchingIsIntersecting) { + assertRangeValid(this); + assertNode(node, "NOT_FOUND_ERR"); + if (dom.getDocument(node) !== getRangeDocument(this)) { + return false; + } + + var parent = node.parentNode, offset = dom.getNodeIndex(node); + assertNode(parent, "NOT_FOUND_ERR"); + + var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, + + + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, + + // The methods below are non-standard and invented by me. + + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range, touchingIsIntersecting) { + assertRangeValid(this); + + if (getRangeDocument(range) != getRangeDocument(this)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + + var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset), + endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, + + intersection: function(range) { + if (this.intersectsRange(range)) { + var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), + endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); + + var intersectionRange = this.cloneRange(); + + if (startComparison == -1) { + intersectionRange.setStart(range.startContainer, range.startOffset); + } + if (endComparison == 1) { + intersectionRange.setEnd(range.endContainer, range.endOffset); + } + return intersectionRange; + } + return null; + }, + + union: function(range) { + if (this.intersectsRange(range, true)) { + var unionRange = this.cloneRange(); + if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { + unionRange.setStart(range.startContainer, range.startOffset); + } + if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { + unionRange.setEnd(range.endContainer, range.endOffset); + } + return unionRange; + } else { + throw new RangeException("Ranges do not intersect"); + } + }, + + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, + + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0; + }, + + containsRange: function(range) { + return this.intersection(range).equals(range); + }, + + containsNodeText: function(node) { + var nodeRange = this.cloneRange(); + nodeRange.selectNode(node); + var textNodes = nodeRange.getNodes([3]); + if (textNodes.length > 0) { + nodeRange.setStart(textNodes[0], 0); + var lastTextNode = textNodes.pop(); + nodeRange.setEnd(lastTextNode, lastTextNode.length); + var contains = this.containsRange(nodeRange); + nodeRange.detach(); + return contains; + } else { + return this.containsNodeContents(node); + } + }, + + createNodeIterator: function(nodeTypes, filter) { + assertRangeValid(this); + return new RangeNodeIterator(this, nodeTypes, filter); + }, + + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, + + getDocument: function() { + return getRangeDocument(this); + }, + + collapseBefore: function(node) { + assertNotDetached(this); + + this.setEndBefore(node); + this.collapse(false); + }, + + collapseAfter: function(node) { + assertNotDetached(this); + + this.setStartAfter(node); + this.collapse(true); + }, + + getName: function() { + return "DomRange"; + }, + + equals: function(range) { + return Range.rangesEqual(this, range); + }, + + inspect: function() { + return inspect(this); + } + }; + + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; + + obj.NODE_BEFORE = n_b; + obj.NODE_AFTER = n_a; + obj.NODE_BEFORE_AND_AFTER = n_b_a; + obj.NODE_INSIDE = n_i; + } + + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); + } + + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + + var iterator = new RangeIterator(this, true); + + // Work out where to position the range after content removal + var node, boundary; + if (sc !== root) { + node = dom.getClosestAncestorIn(sc, root, true); + boundary = getBoundaryAfterNode(node); + sc = boundary.node; + so = boundary.offset; + } + + // Check none of the range is read-only + iterateSubtree(iterator, assertNodeNotReadOnly); + + iterator.reset(); + + // Remove the content + var returnValue = remover(iterator); + iterator.detach(); + + // Move to the new position + boundaryUpdater(this, sc, so, sc, so); + + return returnValue; + }; + } + + function createPrototypeRange(constructor, boundaryUpdater, detacher) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { + assertNotDetached(this); + assertValidNodeType(node, beforeAfterNodeTypes); + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + }; + } + + function setRangeStart(range, node, offset) { + var ec = range.endContainer, eo = range.endOffset; + if (node !== range.startContainer || offset !== range.startOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) { + ec = node; + eo = offset; + } + boundaryUpdater(range, node, offset, ec, eo); + } + } + + function setRangeEnd(range, node, offset) { + var sc = range.startContainer, so = range.startOffset; + if (node !== range.endContainer || offset !== range.endOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) { + sc = node; + so = offset; + } + boundaryUpdater(range, sc, so, node, offset); + } + } + + function setRangeStartAndEnd(range, node, offset) { + if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) { + boundaryUpdater(range, node, offset, node, offset); + } + } + + constructor.prototype = new RangePrototype(); + + api.util.extend(constructor.prototype, { + setStart: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStart(this, node, offset); + }, + + setEnd: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeEnd(this, node, offset); + }, + + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), + + collapse: function(isStart) { + assertRangeValid(this); + if (isStart) { + boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); + } else { + boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); + } + }, + + selectNodeContents: function(node) { + // This doesn't seem well specified: the spec talks only about selecting the node's contents, which + // could be taken to mean only its children. However, browsers implement this the same as selectNode for + // text nodes, so I shall do likewise + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + + boundaryUpdater(this, node, 0, node, dom.getNodeLength(node)); + }, + + selectNode: function(node) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, false); + assertValidNodeType(node, beforeAfterNodeTypes); + + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); + }, + + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), + + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + detach: function() { + detacher(this); + }, + + splitBoundaries: function() { + assertRangeValid(this); + + + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + var startEndSame = (sc === ec); + + if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { + dom.splitDataNode(ec, eo); + + } + + if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) { + + sc = dom.splitDataNode(sc, so); + if (startEndSame) { + eo -= so; + ec = sc; + } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) { + eo++; + } + so = 0; + + } + boundaryUpdater(this, sc, so, ec, eo); + }, + + normalizeBoundaries: function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + + var mergeForward = function(node) { + var sibling = node.nextSibling; + if (sibling && sibling.nodeType == node.nodeType) { + ec = node; + eo = node.length; + node.appendData(sibling.data); + sibling.parentNode.removeChild(sibling); + } + }; + + var mergeBackward = function(node) { + var sibling = node.previousSibling; + if (sibling && sibling.nodeType == node.nodeType) { + sc = node; + var nodeLength = node.length; + so = sibling.length; + node.insertData(0, sibling.data); + sibling.parentNode.removeChild(sibling); + if (sc == ec) { + eo += so; + ec = sc; + } else if (ec == node.parentNode) { + var nodeIndex = dom.getNodeIndex(node); + if (eo == nodeIndex) { + ec = node; + eo = nodeLength; + } else if (eo > nodeIndex) { + eo--; + } + } + } + }; + + var normalizeStart = true; + + if (dom.isCharacterDataNode(ec)) { + if (ec.length == eo) { + mergeForward(ec); + } + } else { + if (eo > 0) { + var endNode = ec.childNodes[eo - 1]; + if (endNode && dom.isCharacterDataNode(endNode)) { + mergeForward(endNode); + } + } + normalizeStart = !this.collapsed; + } + + if (normalizeStart) { + if (dom.isCharacterDataNode(sc)) { + if (so == 0) { + mergeBackward(sc); + } + } else { + if (so < sc.childNodes.length) { + var startNode = sc.childNodes[so]; + if (startNode && dom.isCharacterDataNode(startNode)) { + mergeBackward(startNode); + } + } + } + } else { + sc = ec; + so = eo; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + collapseToPoint: function(node, offset) { + assertNotDetached(this); + + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStartAndEnd(this, node, offset); + } + }); + + copyComparisonConstants(constructor); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Updates commonAncestorContainer and collapsed after boundary change + function updateCollapsedAndCommonAncestor(range) { + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + range.commonAncestorContainer = range.collapsed ? + range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); + } + + function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { + var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset); + var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset); + + range.startContainer = startContainer; + range.startOffset = startOffset; + range.endContainer = endContainer; + range.endOffset = endOffset; + + updateCollapsedAndCommonAncestor(range); + dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved}); + } + + function detach(range) { + assertNotDetached(range); + range.startContainer = range.startOffset = range.endContainer = range.endOffset = null; + range.collapsed = range.commonAncestorContainer = null; + dispatchEvent(range, "detach", null); + range._listeners = null; + } + + /** + * @constructor + */ + function Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this._listeners = { + boundarychange: [], + detach: [] + }; + updateCollapsedAndCommonAncestor(this); + } + + createPrototypeRange(Range, updateBoundaries, detach); + + api.rangePrototype = RangePrototype.prototype; + + Range.rangeProperties = rangeProperties; + Range.RangeIterator = RangeIterator; + Range.copyComparisonConstants = copyComparisonConstants; + Range.createPrototypeRange = createPrototypeRange; + Range.inspect = inspect; + Range.getRangeDocument = getRangeDocument; + Range.rangesEqual = function(r1, r2) { + return r1.startContainer === r2.startContainer && + r1.startOffset === r2.startOffset && + r1.endContainer === r2.endContainer && + r1.endOffset === r2.endOffset; + }; + + api.DomRange = Range; + api.RangeException = RangeException; +});rangy.createModule("WrappedRange", function(api, module) { + api.requireModules( ["DomUtil", "DomRange"] ); + + /** + * @constructor + */ + var WrappedRange; + var dom = api.dom; + var DomPosition = dom.DomPosition; + var DomRange = api.DomRange; + + + + /*----------------------------------------------------------------------------------------------------------------*/ + + /* + This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() + method. For example, in the following (where pipes denote the selection boundaries): + + <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul> + + var range = document.selection.createRange(); + alert(range.parentElement().id); // Should alert "ul" but alerts "b" + + This method returns the common ancestor node of the following: + - the parentElement() of the textRange + - the parentElement() of the textRange after calling collapse(true) + - the parentElement() of the textRange after calling collapse(false) + */ + function getTextRangeContainerElement(textRange) { + var parentEl = textRange.parentElement(); + + var range = textRange.duplicate(); + range.collapse(true); + var startEl = range.parentElement(); + range = textRange.duplicate(); + range.collapse(false); + var endEl = range.parentElement(); + var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); + + return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); + } + + function textRangeIsCollapsed(textRange) { + return textRange.compareEndPoints("StartToEnd", textRange) == 0; + } + + // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as + // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has + // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling + // for inputs and images, plus optimizations. + function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) { + var workingRange = textRange.duplicate(); + + workingRange.collapse(isStart); + var containerElement = workingRange.parentElement(); + + // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so + // check for that + // TODO: Find out when. Workaround for wholeRangeContainerElement may break this + if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) { + containerElement = wholeRangeContainerElement; + + } + + + + // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and + // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx + if (!containerElement.canHaveHTML) { + return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); + } + + var workingNode = dom.getDocument(containerElement).createElement("span"); + var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; + var previousNode, nextNode, boundaryPosition, boundaryNode; + + // Move the working range through the container's children, starting at the end and working backwards, until the + // working range reaches or goes past the boundary we're interested in + do { + containerElement.insertBefore(workingNode, workingNode.previousSibling); + workingRange.moveToElementText(workingNode); + } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 && + workingNode.previousSibling); + + // We've now reached or gone past the boundary of the text range we're interested in + // so have identified the node we want + boundaryNode = workingNode.nextSibling; + + if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) { + // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the + // node containing the text range's boundary, so we move the end of the working range to the boundary point + // and measure the length of its text to get the boundary's offset within the node. + workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); + + + var offset; + + if (/[\r\n]/.test(boundaryNode.data)) { + /* + For the particular case of a boundary within a text node containing line breaks (within a <pre> element, + for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts: + + - Each line break is represented as \r in the text node's data/nodeValue properties + - Each line break is represented as \r\n in the TextRange's 'text' property + - The 'text' property of the TextRange does not contain trailing line breaks + + To get round the problem presented by the final fact above, we can use the fact that TextRange's + moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily + the same as the number of characters it was instructed to move. The simplest approach is to use this to + store the characters moved when moving both the start and end of the range to the start of the document + body and subtracting the start offset from the end offset (the "move-negative-gazillion" method). + However, this is extremely slow when the document is large and the range is near the end of it. Clearly + doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same + problem. + + Another approach that works is to use moveStart() to move the start boundary of the range up to the end + boundary one character at a time and incrementing a counter with the value returned by the moveStart() + call. However, the check for whether the start boundary has reached the end boundary is expensive, so + this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of + the range within the document). + + The method below is a hybrid of the two methods above. It uses the fact that a string containing the + TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the + text of the TextRange, so the start of the range is moved that length initially and then a character at + a time to make up for any trailing line breaks not contained in the 'text' property. This has good + performance in most situations compared to the previous two methods. + */ + var tempRange = workingRange.duplicate(); + var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length; + + offset = tempRange.moveStart("character", rangeLength); + while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) { + offset++; + tempRange.moveStart("character", 1); + } + } else { + offset = workingRange.text.length; + } + boundaryPosition = new DomPosition(boundaryNode, offset); + } else { + + + // If the boundary immediately follows a character data node and this is the end boundary, we should favour + // a position within that, and likewise for a start boundary preceding a character data node + previousNode = (isCollapsed || !isStart) && workingNode.previousSibling; + nextNode = (isCollapsed || isStart) && workingNode.nextSibling; + + + + if (nextNode && dom.isCharacterDataNode(nextNode)) { + boundaryPosition = new DomPosition(nextNode, 0); + } else if (previousNode && dom.isCharacterDataNode(previousNode)) { + boundaryPosition = new DomPosition(previousNode, previousNode.length); + } else { + boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode)); + } + } + + // Clean up + workingNode.parentNode.removeChild(workingNode); + + return boundaryPosition; + } + + // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node. + // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange + // (http://code.google.com/p/ierange/) + function createBoundaryTextRange(boundaryPosition, isStart) { + var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset; + var doc = dom.getDocument(boundaryPosition.node); + var workingNode, childNodes, workingRange = doc.body.createTextRange(); + var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node); + + if (nodeIsDataNode) { + boundaryNode = boundaryPosition.node; + boundaryParent = boundaryNode.parentNode; + } else { + childNodes = boundaryPosition.node.childNodes; + boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null; + boundaryParent = boundaryPosition.node; + } + + // Position the range immediately before the node containing the boundary + workingNode = doc.createElement("span"); + + // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the + // element rather than immediately before or after it, which is what we want + workingNode.innerHTML = "&#feff;"; + + // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report + // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12 + if (boundaryNode) { + boundaryParent.insertBefore(workingNode, boundaryNode); + } else { + boundaryParent.appendChild(workingNode); + } + + workingRange.moveToElementText(workingNode); + workingRange.collapse(!isStart); + + // Clean up + boundaryParent.removeChild(workingNode); + + // Move the working range to the text offset, if required + if (nodeIsDataNode) { + workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset); + } + + return workingRange; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) { + // This is a wrapper around the browser's native DOM Range. It has two aims: + // - Provide workarounds for specific browser bugs + // - provide convenient extensions, which are inherited from Rangy's DomRange + + (function() { + var rangeProto; + var rangeProperties = DomRange.rangeProperties; + var canSetRangeStartAfterEnd; + + function updateRangeProperties(range) { + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = range.nativeRange[prop]; + } + } + + function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) { + var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); + var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); + + // Always set both boundaries for the benefit of IE9 (see issue 35) + if (startMoved || endMoved) { + range.setEnd(endContainer, endOffset); + range.setStart(startContainer, startOffset); + } + } + + function detach(range) { + range.nativeRange.detach(); + range.detached = true; + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = null; + } + } + + var createBeforeAfterNodeSetter; + + WrappedRange = function(range) { + if (!range) { + throw new Error("Range must be specified"); + } + this.nativeRange = range; + updateRangeProperties(this); + }; + + DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach); + + rangeProto = WrappedRange.prototype; + + rangeProto.selectNode = function(node) { + this.nativeRange.selectNode(node); + updateRangeProperties(this); + }; + + rangeProto.deleteContents = function() { + this.nativeRange.deleteContents(); + updateRangeProperties(this); + }; + + rangeProto.extractContents = function() { + var frag = this.nativeRange.extractContents(); + updateRangeProperties(this); + return frag; + }; + + rangeProto.cloneContents = function() { + return this.nativeRange.cloneContents(); + }; + + // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still + // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for + // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of + // insertNode, which works but is almost certainly slower than the native implementation. +/* + rangeProto.insertNode = function(node) { + this.nativeRange.insertNode(node); + updateRangeProperties(this); + }; +*/ + + rangeProto.surroundContents = function(node) { + this.nativeRange.surroundContents(node); + updateRangeProperties(this); + }; + + rangeProto.collapse = function(isStart) { + this.nativeRange.collapse(isStart); + updateRangeProperties(this); + }; + + rangeProto.cloneRange = function() { + return new WrappedRange(this.nativeRange.cloneRange()); + }; + + rangeProto.refresh = function() { + updateRangeProperties(this); + }; + + rangeProto.toString = function() { + return this.nativeRange.toString(); + }; + + // Create test range and node for feature detection + + var testTextNode = document.createTextNode("test"); + dom.getBody(document).appendChild(testTextNode); + var range = document.createRange(); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and + // correct for it + + range.setStart(testTextNode, 0); + range.setEnd(testTextNode, 0); + + try { + range.setStart(testTextNode, 1); + canSetRangeStartAfterEnd = true; + + rangeProto.setStart = function(node, offset) { + this.nativeRange.setStart(node, offset); + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + this.nativeRange.setEnd(node, offset); + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name) { + return function(node) { + this.nativeRange[name](node); + updateRangeProperties(this); + }; + }; + + } catch(ex) { + + + canSetRangeStartAfterEnd = false; + + rangeProto.setStart = function(node, offset) { + try { + this.nativeRange.setStart(node, offset); + } catch (ex) { + this.nativeRange.setEnd(node, offset); + this.nativeRange.setStart(node, offset); + } + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + try { + this.nativeRange.setEnd(node, offset); + } catch (ex) { + this.nativeRange.setStart(node, offset); + this.nativeRange.setEnd(node, offset); + } + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name, oppositeName) { + return function(node) { + try { + this.nativeRange[name](node); + } catch (ex) { + this.nativeRange[oppositeName](node); + this.nativeRange[name](node); + } + updateRangeProperties(this); + }; + }; + } + + rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); + rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); + rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); + rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to + // the 0th character of the text node + range.selectNodeContents(testTextNode); + if (range.startContainer == testTextNode && range.endContainer == testTextNode && + range.startOffset == 0 && range.endOffset == testTextNode.length) { + rangeProto.selectNodeContents = function(node) { + this.nativeRange.selectNodeContents(node); + updateRangeProperties(this); + }; + } else { + rangeProto.selectNodeContents = function(node) { + this.setStart(node, 0); + this.setEnd(node, DomRange.getEndOffset(node)); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants + // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 + + range.selectNodeContents(testTextNode); + range.setEnd(testTextNode, 3); + + var range2 = document.createRange(); + range2.selectNodeContents(testTextNode); + range2.setEnd(testTextNode, 4); + range2.setStart(testTextNode, 2); + + if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 & + range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { + // This is the wrong way round, so correct for it + + + rangeProto.compareBoundaryPoints = function(type, range) { + range = range.nativeRange || range; + if (type == range.START_TO_END) { + type = range.END_TO_START; + } else if (type == range.END_TO_START) { + type = range.START_TO_END; + } + return this.nativeRange.compareBoundaryPoints(type, range); + }; + } else { + rangeProto.compareBoundaryPoints = function(type, range) { + return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for existence of createContextualFragment and delegate to it if it exists + if (api.util.isHostMethod(range, "createContextualFragment")) { + rangeProto.createContextualFragment = function(fragmentStr) { + return this.nativeRange.createContextualFragment(fragmentStr); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Clean up + dom.getBody(document).removeChild(testTextNode); + range.detach(); + range2.detach(); + })(); + + api.createNativeRange = function(doc) { + doc = doc || document; + return doc.createRange(); + }; + } else if (api.features.implementsTextRange) { + // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a + // prototype + + WrappedRange = function(textRange) { + this.textRange = textRange; + this.refresh(); + }; + + WrappedRange.prototype = new DomRange(document); + + WrappedRange.prototype.refresh = function() { + var start, end; + + // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that. + var rangeContainerElement = getTextRangeContainerElement(this.textRange); + + if (textRangeIsCollapsed(this.textRange)) { + end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true); + } else { + + start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false); + end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false); + } + + this.setStart(start.node, start.offset); + this.setEnd(end.node, end.offset); + }; + + DomRange.copyComparisonConstants(WrappedRange); + + // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work + var globalObj = (function() { return this; })(); + if (typeof globalObj.Range == "undefined") { + globalObj.Range = WrappedRange; + } + + api.createNativeRange = function(doc) { + doc = doc || document; + return doc.body.createTextRange(); + }; + } + + if (api.features.implementsTextRange) { + WrappedRange.rangeToTextRange = function(range) { + if (range.collapsed) { + var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); + + + + return tr; + + //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); + } else { + var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); + var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false); + var textRange = dom.getDocument(range.startContainer).body.createTextRange(); + textRange.setEndPoint("StartToStart", startRange); + textRange.setEndPoint("EndToEnd", endRange); + return textRange; + } + }; + } + + WrappedRange.prototype.getName = function() { + return "WrappedRange"; + }; + + api.WrappedRange = WrappedRange; + + api.createRange = function(doc) { + doc = doc || document; + return new WrappedRange(api.createNativeRange(doc)); + }; + + api.createRangyRange = function(doc) { + doc = doc || document; + return new DomRange(doc); + }; + + api.createIframeRange = function(iframeEl) { + return api.createRange(dom.getIframeDocument(iframeEl)); + }; + + api.createIframeRangyRange = function(iframeEl) { + return api.createRangyRange(dom.getIframeDocument(iframeEl)); + }; + + api.addCreateMissingNativeApiListener(function(win) { + var doc = win.document; + if (typeof doc.createRange == "undefined") { + doc.createRange = function() { + return api.createRange(this); + }; + } + doc = win = null; + }); +});rangy.createModule("WrappedSelection", function(api, module) { + // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range + // spec (http://html5.org/specs/dom-range.html) + + api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] ); + + api.config.checkSelectionRanges = true; + + var BOOLEAN = "boolean", + windowPropertyName = "_rangySelection", + dom = api.dom, + util = api.util, + DomRange = api.DomRange, + WrappedRange = api.WrappedRange, + DOMException = api.DOMException, + DomPosition = dom.DomPosition, + getSelection, + selectionIsCollapsed, + CONTROL = "Control"; + + + + function getWinSelection(winParam) { + return (winParam || window).getSelection(); + } + + function getDocSelection(winParam) { + return (winParam || window).document.selection; + } + + // Test for the Range/TextRange and Selection features required + // Test for ability to retrieve selection + var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"), + implementsDocSelection = api.util.isHostObject(document, "selection"); + + var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange); + + if (useDocumentSelection) { + getSelection = getDocSelection; + api.isSelectionValid = function(winParam) { + var doc = (winParam || window).document, nativeSel = doc.selection; + + // Check whether the selection TextRange is actually contained within the correct document + return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc); + }; + } else if (implementsWinGetSelection) { + getSelection = getWinSelection; + api.isSelectionValid = function() { + return true; + }; + } else { + module.fail("Neither document.selection or window.getSelection() detected."); + } + + api.getNativeSelection = getSelection; + + var testSelection = getSelection(); + var testRange = api.createNativeRange(document); + var body = dom.getBody(document); + + // Obtaining a range from a selection + var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] && + util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"])); + api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus; + + // Test for existence of native selection extend() method + var selectionHasExtend = util.isHostMethod(testSelection, "extend"); + api.features.selectionHasExtend = selectionHasExtend; + + // Test if rangeCount exists + var selectionHasRangeCount = (typeof testSelection.rangeCount == "number"); + api.features.selectionHasRangeCount = selectionHasRangeCount; + + var selectionSupportsMultipleRanges = false; + var collapsedNonEditableSelectionsSupported = true; + + if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) && + typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) { + + (function() { + var iframe = document.createElement("iframe"); + body.appendChild(iframe); + + var iframeDoc = dom.getIframeDocument(iframe); + iframeDoc.open(); + iframeDoc.write("<html><head></head><body>12</body></html>"); + iframeDoc.close(); + + var sel = dom.getIframeWindow(iframe).getSelection(); + var docEl = iframeDoc.documentElement; + var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild; + + // Test whether the native selection will allow a collapsed selection within a non-editable element + var r1 = iframeDoc.createRange(); + r1.setStart(textNode, 1); + r1.collapse(true); + sel.addRange(r1); + collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1); + sel.removeAllRanges(); + + // Test whether the native selection is capable of supporting multiple ranges + var r2 = r1.cloneRange(); + r1.setStart(textNode, 0); + r2.setEnd(textNode, 2); + sel.addRange(r1); + sel.addRange(r2); + + selectionSupportsMultipleRanges = (sel.rangeCount == 2); + + // Clean up + r1.detach(); + r2.detach(); + + body.removeChild(iframe); + })(); + } + + api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges; + api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported; + + // ControlRanges + var implementsControlRange = false, testControlRange; + + if (body && util.isHostMethod(body, "createControlRange")) { + testControlRange = body.createControlRange(); + if (util.areHostProperties(testControlRange, ["item", "add"])) { + implementsControlRange = true; + } + } + api.features.implementsControlRange = implementsControlRange; + + // Selection collapsedness + if (selectionHasAnchorAndFocus) { + selectionIsCollapsed = function(sel) { + return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset; + }; + } else { + selectionIsCollapsed = function(sel) { + return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false; + }; + } + + function updateAnchorAndFocusFromRange(sel, range, backwards) { + var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end"; + sel.anchorNode = range[anchorPrefix + "Container"]; + sel.anchorOffset = range[anchorPrefix + "Offset"]; + sel.focusNode = range[focusPrefix + "Container"]; + sel.focusOffset = range[focusPrefix + "Offset"]; + } + + function updateAnchorAndFocusFromNativeSelection(sel) { + var nativeSel = sel.nativeSelection; + sel.anchorNode = nativeSel.anchorNode; + sel.anchorOffset = nativeSel.anchorOffset; + sel.focusNode = nativeSel.focusNode; + sel.focusOffset = nativeSel.focusOffset; + } + + function updateEmptySelection(sel) { + sel.anchorNode = sel.focusNode = null; + sel.anchorOffset = sel.focusOffset = 0; + sel.rangeCount = 0; + sel.isCollapsed = true; + sel._ranges.length = 0; + } + + function getNativeRange(range) { + var nativeRange; + if (range instanceof DomRange) { + nativeRange = range._selectionNativeRange; + if (!nativeRange) { + nativeRange = api.createNativeRange(dom.getDocument(range.startContainer)); + nativeRange.setEnd(range.endContainer, range.endOffset); + nativeRange.setStart(range.startContainer, range.startOffset); + range._selectionNativeRange = nativeRange; + range.attachListener("detach", function() { + + this._selectionNativeRange = null; + }); + } + } else if (range instanceof WrappedRange) { + nativeRange = range.nativeRange; + } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) { + nativeRange = range; + } + return nativeRange; + } + + function rangeContainsSingleElement(rangeNodes) { + if (!rangeNodes.length || rangeNodes[0].nodeType != 1) { + return false; + } + for (var i = 1, len = rangeNodes.length; i < len; ++i) { + if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) { + return false; + } + } + return true; + } + + function getSingleElementFromRange(range) { + var nodes = range.getNodes(); + if (!rangeContainsSingleElement(nodes)) { + throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element"); + } + return nodes[0]; + } + + function isTextRange(range) { + return !!range && typeof range.text != "undefined"; + } + + function updateFromTextRange(sel, range) { + // Create a Range from the selected TextRange + var wrappedRange = new WrappedRange(range); + sel._ranges = [wrappedRange]; + + updateAnchorAndFocusFromRange(sel, wrappedRange, false); + sel.rangeCount = 1; + sel.isCollapsed = wrappedRange.collapsed; + } + + function updateControlSelection(sel) { + // Update the wrapped selection based on what's now in the native selection + sel._ranges.length = 0; + if (sel.docSelection.type == "None") { + updateEmptySelection(sel); + } else { + var controlRange = sel.docSelection.createRange(); + if (isTextRange(controlRange)) { + // This case (where the selection type is "Control" and calling createRange() on the selection returns + // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected + // ControlRange have been removed from the ControlRange and removed from the document. + updateFromTextRange(sel, controlRange); + } else { + sel.rangeCount = controlRange.length; + var range, doc = dom.getDocument(controlRange.item(0)); + for (var i = 0; i < sel.rangeCount; ++i) { + range = api.createRange(doc); + range.selectNode(controlRange.item(i)); + sel._ranges.push(range); + } + sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; + updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); + } + } + } + + function addRangeToControlSelection(sel, range) { + var controlRange = sel.docSelection.createRange(); + var rangeElement = getSingleElementFromRange(range); + + // Create a new ControlRange containing all the elements in the selected ControlRange plus the element + // contained by the supplied range + var doc = dom.getDocument(controlRange.item(0)); + var newControlRange = dom.getBody(doc).createControlRange(); + for (var i = 0, len = controlRange.length; i < len; ++i) { + newControlRange.add(controlRange.item(i)); + } + try { + newControlRange.add(rangeElement); + } catch (ex) { + throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); + } + newControlRange.select(); + + // Update the wrapped selection based on what's now in the native selection + updateControlSelection(sel); + } + + var getSelectionRangeAt; + + if (util.isHostMethod(testSelection, "getRangeAt")) { + getSelectionRangeAt = function(sel, index) { + try { + return sel.getRangeAt(index); + } catch(ex) { + return null; + } + }; + } else if (selectionHasAnchorAndFocus) { + getSelectionRangeAt = function(sel) { + var doc = dom.getDocument(sel.anchorNode); + var range = api.createRange(doc); + range.setStart(sel.anchorNode, sel.anchorOffset); + range.setEnd(sel.focusNode, sel.focusOffset); + + // Handle the case when the selection was selected backwards (from the end to the start in the + // document) + if (range.collapsed !== this.isCollapsed) { + range.setStart(sel.focusNode, sel.focusOffset); + range.setEnd(sel.anchorNode, sel.anchorOffset); + } + + return range; + }; + } + + /** + * @constructor + */ + function WrappedSelection(selection, docSelection, win) { + this.nativeSelection = selection; + this.docSelection = docSelection; + this._ranges = []; + this.win = win; + this.refresh(); + } + + api.getSelection = function(win) { + win = win || window; + var sel = win[windowPropertyName]; + var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null; + if (sel) { + sel.nativeSelection = nativeSel; + sel.docSelection = docSel; + sel.refresh(win); + } else { + sel = new WrappedSelection(nativeSel, docSel, win); + win[windowPropertyName] = sel; + } + return sel; + }; + + api.getIframeSelection = function(iframeEl) { + return api.getSelection(dom.getIframeWindow(iframeEl)); + }; + + var selProto = WrappedSelection.prototype; + + function createControlSelection(sel, ranges) { + // Ensure that the selection becomes of type "Control" + var doc = dom.getDocument(ranges[0].startContainer); + var controlRange = dom.getBody(doc).createControlRange(); + for (var i = 0, el; i < rangeCount; ++i) { + el = getSingleElementFromRange(ranges[i]); + try { + controlRange.add(el); + } catch (ex) { + throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)"); + } + } + controlRange.select(); + + // Update the wrapped selection based on what's now in the native selection + updateControlSelection(sel); + } + + // Selecting a range + if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { + selProto.removeAllRanges = function() { + this.nativeSelection.removeAllRanges(); + updateEmptySelection(this); + }; + + var addRangeBackwards = function(sel, range) { + var doc = DomRange.getRangeDocument(range); + var endRange = api.createRange(doc); + endRange.collapseToPoint(range.endContainer, range.endOffset); + sel.nativeSelection.addRange(getNativeRange(endRange)); + sel.nativeSelection.extend(range.startContainer, range.startOffset); + sel.refresh(); + }; + + if (selectionHasRangeCount) { + selProto.addRange = function(range, backwards) { + if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { + addRangeToControlSelection(this, range); + } else { + if (backwards && selectionHasExtend) { + addRangeBackwards(this, range); + } else { + var previousRangeCount; + if (selectionSupportsMultipleRanges) { + previousRangeCount = this.rangeCount; + } else { + this.removeAllRanges(); + previousRangeCount = 0; + } + this.nativeSelection.addRange(getNativeRange(range)); + + // Check whether adding the range was successful + this.rangeCount = this.nativeSelection.rangeCount; + + if (this.rangeCount == previousRangeCount + 1) { + // The range was added successfully + + // Check whether the range that we added to the selection is reflected in the last range extracted from + // the selection + if (api.config.checkSelectionRanges) { + var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); + if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) { + // Happens in WebKit with, for example, a selection placed at the start of a text node + range = new WrappedRange(nativeRange); + } + } + this._ranges[this.rangeCount - 1] = range; + updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection)); + this.isCollapsed = selectionIsCollapsed(this); + } else { + // The range was not added successfully. The simplest thing is to refresh + this.refresh(); + } + } + } + }; + } else { + selProto.addRange = function(range, backwards) { + if (backwards && selectionHasExtend) { + addRangeBackwards(this, range); + } else { + this.nativeSelection.addRange(getNativeRange(range)); + this.refresh(); + } + }; + } + + selProto.setRanges = function(ranges) { + if (implementsControlRange && ranges.length > 1) { + createControlSelection(this, ranges); + } else { + this.removeAllRanges(); + for (var i = 0, len = ranges.length; i < len; ++i) { + this.addRange(ranges[i]); + } + } + }; + } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") && + implementsControlRange && useDocumentSelection) { + + selProto.removeAllRanges = function() { + // Added try/catch as fix for issue #21 + try { + this.docSelection.empty(); + + // Check for empty() not working (issue #24) + if (this.docSelection.type != "None") { + // Work around failure to empty a control selection by instead selecting a TextRange and then + // calling empty() + var doc; + if (this.anchorNode) { + doc = dom.getDocument(this.anchorNode); + } else if (this.docSelection.type == CONTROL) { + var controlRange = this.docSelection.createRange(); + if (controlRange.length) { + doc = dom.getDocument(controlRange.item(0)).body.createTextRange(); + } + } + if (doc) { + var textRange = doc.body.createTextRange(); + textRange.select(); + this.docSelection.empty(); + } + } + } catch(ex) {} + updateEmptySelection(this); + }; + + selProto.addRange = function(range) { + if (this.docSelection.type == CONTROL) { + addRangeToControlSelection(this, range); + } else { + WrappedRange.rangeToTextRange(range).select(); + this._ranges[0] = range; + this.rangeCount = 1; + this.isCollapsed = this._ranges[0].collapsed; + updateAnchorAndFocusFromRange(this, range, false); + } + }; + + selProto.setRanges = function(ranges) { + this.removeAllRanges(); + var rangeCount = ranges.length; + if (rangeCount > 1) { + createControlSelection(this, ranges); + } else if (rangeCount) { + this.addRange(ranges[0]); + } + }; + } else { + module.fail("No means of selecting a Range or TextRange was found"); + return false; + } + + selProto.getRangeAt = function(index) { + if (index < 0 || index >= this.rangeCount) { + throw new DOMException("INDEX_SIZE_ERR"); + } else { + return this._ranges[index]; + } + }; + + var refreshSelection; + + if (useDocumentSelection) { + refreshSelection = function(sel) { + var range; + if (api.isSelectionValid(sel.win)) { + range = sel.docSelection.createRange(); + } else { + range = dom.getBody(sel.win.document).createTextRange(); + range.collapse(true); + } + + + if (sel.docSelection.type == CONTROL) { + updateControlSelection(sel); + } else if (isTextRange(range)) { + updateFromTextRange(sel, range); + } else { + updateEmptySelection(sel); + } + }; + } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") { + refreshSelection = function(sel) { + if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) { + updateControlSelection(sel); + } else { + sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; + if (sel.rangeCount) { + for (var i = 0, len = sel.rangeCount; i < len; ++i) { + sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); + } + updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection)); + sel.isCollapsed = selectionIsCollapsed(sel); + } else { + updateEmptySelection(sel); + } + } + }; + } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) { + refreshSelection = function(sel) { + var range, nativeSel = sel.nativeSelection; + if (nativeSel.anchorNode) { + range = getSelectionRangeAt(nativeSel, 0); + sel._ranges = [range]; + sel.rangeCount = 1; + updateAnchorAndFocusFromNativeSelection(sel); + sel.isCollapsed = selectionIsCollapsed(sel); + } else { + updateEmptySelection(sel); + } + }; + } else { + module.fail("No means of obtaining a Range or TextRange from the user's selection was found"); + return false; + } + + selProto.refresh = function(checkForChanges) { + var oldRanges = checkForChanges ? this._ranges.slice(0) : null; + refreshSelection(this); + if (checkForChanges) { + var i = oldRanges.length; + if (i != this._ranges.length) { + return false; + } + while (i--) { + if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) { + return false; + } + } + return true; + } + }; + + // Removal of a single range + var removeRangeManually = function(sel, range) { + var ranges = sel.getAllRanges(), removed = false; + sel.removeAllRanges(); + for (var i = 0, len = ranges.length; i < len; ++i) { + if (removed || range !== ranges[i]) { + sel.addRange(ranges[i]); + } else { + // According to the draft WHATWG Range spec, the same range may be added to the selection multiple + // times. removeRange should only remove the first instance, so the following ensures only the first + // instance is removed + removed = true; + } + } + if (!sel.rangeCount) { + updateEmptySelection(sel); + } + }; + + if (implementsControlRange) { + selProto.removeRange = function(range) { + if (this.docSelection.type == CONTROL) { + var controlRange = this.docSelection.createRange(); + var rangeElement = getSingleElementFromRange(range); + + // Create a new ControlRange containing all the elements in the selected ControlRange minus the + // element contained by the supplied range + var doc = dom.getDocument(controlRange.item(0)); + var newControlRange = dom.getBody(doc).createControlRange(); + var el, removed = false; + for (var i = 0, len = controlRange.length; i < len; ++i) { + el = controlRange.item(i); + if (el !== rangeElement || removed) { + newControlRange.add(controlRange.item(i)); + } else { + removed = true; + } + } + newControlRange.select(); + + // Update the wrapped selection based on what's now in the native selection + updateControlSelection(this); + } else { + removeRangeManually(this, range); + } + }; + } else { + selProto.removeRange = function(range) { + removeRangeManually(this, range); + }; + } + + // Detecting if a selection is backwards + var selectionIsBackwards; + if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) { + selectionIsBackwards = function(sel) { + var backwards = false; + if (sel.anchorNode) { + backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1); + } + return backwards; + }; + + selProto.isBackwards = function() { + return selectionIsBackwards(this); + }; + } else { + selectionIsBackwards = selProto.isBackwards = function() { + return false; + }; + } + + // Selection text + // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation + selProto.toString = function() { + + var rangeTexts = []; + for (var i = 0, len = this.rangeCount; i < len; ++i) { + rangeTexts[i] = "" + this._ranges[i]; + } + return rangeTexts.join(""); + }; + + function assertNodeInSameDocument(sel, node) { + if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } + + // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used + selProto.collapse = function(node, offset) { + assertNodeInSameDocument(this, node); + var range = api.createRange(dom.getDocument(node)); + range.collapseToPoint(node, offset); + this.removeAllRanges(); + this.addRange(range); + this.isCollapsed = true; + }; + + selProto.collapseToStart = function() { + if (this.rangeCount) { + var range = this._ranges[0]; + this.collapse(range.startContainer, range.startOffset); + } else { + throw new DOMException("INVALID_STATE_ERR"); + } + }; + + selProto.collapseToEnd = function() { + if (this.rangeCount) { + var range = this._ranges[this.rangeCount - 1]; + this.collapse(range.endContainer, range.endOffset); + } else { + throw new DOMException("INVALID_STATE_ERR"); + } + }; + + // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is + // never used by Rangy. + selProto.selectAllChildren = function(node) { + assertNodeInSameDocument(this, node); + var range = api.createRange(dom.getDocument(node)); + range.selectNodeContents(node); + this.removeAllRanges(); + this.addRange(range); + }; + + selProto.deleteFromDocument = function() { + // Sepcial behaviour required for Control selections + if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { + var controlRange = this.docSelection.createRange(); + var element; + while (controlRange.length) { + element = controlRange.item(0); + controlRange.remove(element); + element.parentNode.removeChild(element); + } + this.refresh(); + } else if (this.rangeCount) { + var ranges = this.getAllRanges(); + this.removeAllRanges(); + for (var i = 0, len = ranges.length; i < len; ++i) { + ranges[i].deleteContents(); + } + // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each + // range. Firefox moves the selection to where the final selected range was, so we emulate that + this.addRange(ranges[len - 1]); + } + }; + + // The following are non-standard extensions + selProto.getAllRanges = function() { + return this._ranges.slice(0); + }; + + selProto.setSingleRange = function(range) { + this.setRanges( [range] ); + }; + + selProto.containsNode = function(node, allowPartial) { + for (var i = 0, len = this._ranges.length; i < len; ++i) { + if (this._ranges[i].containsNode(node, allowPartial)) { + return true; + } + } + return false; + }; + + selProto.toHtml = function() { + var html = ""; + if (this.rangeCount) { + var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div"); + for (var i = 0, len = this._ranges.length; i < len; ++i) { + container.appendChild(this._ranges[i].cloneContents()); + } + html = container.innerHTML; + } + return html; + }; + + function inspect(sel) { + var rangeInspects = []; + var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset); + var focus = new DomPosition(sel.focusNode, sel.focusOffset); + var name = (typeof sel.getName == "function") ? sel.getName() : "Selection"; + + if (typeof sel.rangeCount != "undefined") { + for (var i = 0, len = sel.rangeCount; i < len; ++i) { + rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i)); + } + } + return "[" + name + "(Ranges: " + rangeInspects.join(", ") + + ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]"; + + } + + selProto.getName = function() { + return "WrappedSelection"; + }; + + selProto.inspect = function() { + return inspect(this); + }; + + selProto.detach = function() { + this.win[windowPropertyName] = null; + this.win = this.anchorNode = this.focusNode = null; + }; + + WrappedSelection.inspect = inspect; + + api.Selection = WrappedSelection; + + api.selectionPrototype = selProto; + + api.addCreateMissingNativeApiListener(function(win) { + if (typeof win.getSelection == "undefined") { + win.getSelection = function() { + return api.getSelection(this); + }; + } + win = null; + }); +}); +/* + Base.js, version 1.1a + Copyright 2006-2010, Dean Edwards + License: http://www.opensource.org/licenses/mit-license.php +*/ + +var Base = function() { + // dummy +}; + +Base.extend = function(_instance, _static) { // subclass + var extend = Base.prototype.extend; + + // build the prototype + Base._prototyping = true; + var proto = new this; + extend.call(proto, _instance); + proto.base = function() { + // call this method from any other method to invoke that method's ancestor + }; + delete Base._prototyping; + + // create the wrapper for the constructor function + //var constructor = proto.constructor.valueOf(); //-dean + var constructor = proto.constructor; + var klass = proto.constructor = function() { + if (!Base._prototyping) { + if (this._constructing || this.constructor == klass) { // instantiation + this._constructing = true; + constructor.apply(this, arguments); + delete this._constructing; + } else if (arguments[0] != null) { // casting + return (arguments[0].extend || extend).call(arguments[0], proto); + } + } + }; + + // build the class interface + klass.ancestor = this; + klass.extend = this.extend; + klass.forEach = this.forEach; + klass.implement = this.implement; + klass.prototype = proto; + klass.toString = this.toString; + klass.valueOf = function(type) { + //return (type == "object") ? klass : constructor; //-dean + return (type == "object") ? klass : constructor.valueOf(); + }; + extend.call(klass, _static); + // class initialisation + if (typeof klass.init == "function") klass.init(); + return klass; +}; + +Base.prototype = { + extend: function(source, value) { + if (arguments.length > 1) { // extending with a name/value pair + var ancestor = this[source]; + if (ancestor && (typeof value == "function") && // overriding a method? + // the valueOf() comparison is to avoid circular references + (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) && + /\bbase\b/.test(value)) { + // get the underlying method + var method = value.valueOf(); + // override + value = function() { + var previous = this.base || Base.prototype.base; + this.base = ancestor; + var returnValue = method.apply(this, arguments); + this.base = previous; + return returnValue; + }; + // point to the underlying method + value.valueOf = function(type) { + return (type == "object") ? value : method; + }; + value.toString = Base.toString; + } + this[source] = value; + } else if (source) { // extending with an object literal + var extend = Base.prototype.extend; + // if this object has a customised extend method then use it + if (!Base._prototyping && typeof this != "function") { + extend = this.extend || extend; + } + var proto = {toSource: null}; + // do the "toString" and other methods manually + var hidden = ["constructor", "toString", "valueOf"]; + // if we are prototyping then include the constructor + var i = Base._prototyping ? 0 : 1; + while (key = hidden[i++]) { + if (source[key] != proto[key]) { + extend.call(this, key, source[key]); + + } + } + // copy each of the source object's properties to this object + for (var key in source) { + if (!proto[key]) extend.call(this, key, source[key]); + } + } + return this; + } +}; + +// initialise +Base = Base.extend({ + constructor: function() { + this.extend(arguments[0]); + } +}, { + ancestor: Object, + version: "1.1", + + forEach: function(object, block, context) { + for (var key in object) { + if (this.prototype[key] === undefined) { + block.call(context, object[key], key, object); + } + } + }, + + implement: function() { + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] == "function") { + // if it's a function, call it + arguments[i](this.prototype); + } else { + // add the interface using the extend method + this.prototype.extend(arguments[i]); + } + } + return this; + }, + + toString: function() { + return String(this.valueOf()); + } +});/** + * Detect browser support for specific features + */ +wysihtml5.browser = (function() { + var userAgent = navigator.userAgent, + testElement = document.createElement("div"), + // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect + isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1, + isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1, + isWebKit = userAgent.indexOf("AppleWebKit/") !== -1, + isChrome = userAgent.indexOf("Chrome/") !== -1, + isOpera = userAgent.indexOf("Opera/") !== -1; + + function iosVersion(userAgent) { + return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1]; + } + + return { + // Static variable needed, publicly accessible, to be able override it in unit tests + USER_AGENT: userAgent, + + /** + * Exclude browsers that are not capable of displaying and handling + * contentEditable as desired: + * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable + * - IE < 8 create invalid markup and crash randomly from time to time + * + * @return {Boolean} + */ + supported: function() { + var userAgent = this.USER_AGENT.toLowerCase(), + // Essential for making html elements editable + hasContentEditableSupport = "contentEditable" in testElement, + // Following methods are needed in order to interact with the contentEditable area + hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState, + // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+ + hasQuerySelectorSupport = document.querySelector && document.querySelectorAll, + // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05) + isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; + + return hasContentEditableSupport + && hasEditingApiSupport + && hasQuerySelectorSupport + && !isIncompatibleMobileBrowser; + }, + + isTouchDevice: function() { + return this.supportsEvent("touchmove"); + }, + + isIos: function() { + var userAgent = this.USER_AGENT.toLowerCase(); + return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1; + }, + + /** + * Whether the browser supports sandboxed iframes + * Currently only IE 6+ offers such feature <iframe security="restricted"> + * + * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx + * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx + * + * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage) + */ + supportsSandboxedIframes: function() { + return isIE; + }, + + /** + * IE6+7 throw a mixed content warning when the src of an iframe + * is empty/unset or about:blank + * window.querySelector is implemented as of IE8 + */ + throwsMixedContentWarningWhenIframeSrcIsEmpty: function() { + return !("querySelector" in document); + }, + + /** + * Whether the caret is correctly displayed in contentEditable elements + * Firefox sometimes shows a huge caret in the beginning after focusing + */ + displaysCaretInEmptyContentEditableCorrectly: function() { + return !isGecko; + }, + + /** + * Opera and IE are the only browsers who offer the css value + * in the original unit, thx to the currentStyle object + * All other browsers provide the computed style in px via window.getComputedStyle + */ + hasCurrentStyleProperty: function() { + return "currentStyle" in testElement; + }, + + /** + * Whether the browser inserts a <br> when pressing enter in a contentEditable element + */ + insertsLineBreaksOnReturn: function() { + return isGecko; + }, + + supportsPlaceholderAttributeOn: function(element) { + return "placeholder" in element; + }, + + supportsEvent: function(eventName) { + return "on" + eventName in testElement || (function() { + testElement.setAttribute("on" + eventName, "return;"); + return typeof(testElement["on" + eventName]) === "function"; + })(); + }, + + /** + * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe + */ + supportsEventsInIframeCorrectly: function() { + return !isOpera; + }, + + /** + * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled + * with event.preventDefault + * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs + * to be cancelled + */ + firesOnDropOnlyWhenOnDragOverIsCancelled: function() { + return isWebKit || isGecko; + }, + + /** + * Whether the browser supports the event.dataTransfer property in a proper way + */ + supportsDataTransfer: function() { + try { + // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does) + return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData; + } catch(e) { + return false; + } + }, + + /** + * Everything below IE9 doesn't know how to treat HTML5 tags + * + * @param {Object} context The document object on which to check HTML5 support + * + * @example + * wysihtml5.browser.supportsHTML5Tags(document); + */ + supportsHTML5Tags: function(context) { + var element = context.createElement("div"), + html5 = "<article>foo</article>"; + element.innerHTML = html5; + return element.innerHTML.toLowerCase() === html5; + }, + + /** + * Checks whether a document supports a certain queryCommand + * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree + * in oder to report correct results + * + * @param {Object} doc Document object on which to check for a query command + * @param {String} command The query command to check for + * @return {Boolean} + * + * @example + * wysihtml5.browser.supportsCommand(document, "bold"); + */ + supportsCommand: (function() { + // Following commands are supported but contain bugs in some browsers + var buggyCommands = { + // formatBlock fails with some tags (eg. <blockquote>) + "formatBlock": isIE, + // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets + // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>) + // IE and Opera act a bit different here as they convert the entire content of the current block element into a list + "insertUnorderedList": isIE || isOpera || isWebKit, + "insertOrderedList": isIE || isOpera || isWebKit + }; + + // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands + var supported = { + "insertHTML": isGecko + }; + + return function(doc, command) { + var isBuggy = buggyCommands[command]; + if (!isBuggy) { + // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled + try { + return doc.queryCommandSupported(command); + } catch(e1) {} + + try { + return doc.queryCommandEnabled(command); + } catch(e2) { + return !!supported[command]; + } + } + return false; + }; + })(), + + /** + * IE: URLs starting with: + * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://, + * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url: + * will automatically be auto-linked when either the user inserts them via copy&paste or presses the + * space bar when the caret is directly after such an url. + * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll + * (related blog post on msdn + * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx). + */ + doesAutoLinkingInContentEditable: function() { + return isIE; + }, + + /** + * As stated above, IE auto links urls typed into contentEditable elements + * Since IE9 it's possible to prevent this behavior + */ + canDisableAutoLinking: function() { + return this.supportsCommand(document, "AutoUrlDetect"); + }, + + /** + * IE leaves an empty paragraph in the contentEditable element after clearing it + * Chrome/Safari sometimes an empty <div> + */ + clearsContentEditableCorrectly: function() { + return isGecko || isOpera || isWebKit; + }, + + /** + * IE gives wrong results for getAttribute + */ + supportsGetAttributeCorrectly: function() { + var td = document.createElement("td"); + return td.getAttribute("rowspan") != "1"; + }, + + /** + * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them. + * Chrome and Safari both don't support this + */ + canSelectImagesInContentEditable: function() { + return isGecko || isIE || isOpera; + }, + + /** + * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container + * pressing backspace doesn't remove the entire list as done in other browsers + */ + clearsListsInContentEditableCorrectly: function() { + return isGecko || isIE || isWebKit; + }, + + /** + * All browsers except Safari and Chrome automatically scroll the range/caret position into view + */ + autoScrollsToCaret: function() { + return !isWebKit; + }, + + /** + * Check whether the browser automatically closes tags that don't need to be opened + */ + autoClosesUnclosedTags: function() { + var clonedTestElement = testElement.cloneNode(false), + returnValue, + innerHTML; + + clonedTestElement.innerHTML = "<p><div></div>"; + innerHTML = clonedTestElement.innerHTML.toLowerCase(); + returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>"; + + // Cache result by overwriting current function + this.autoClosesUnclosedTags = function() { return returnValue; }; + + return returnValue; + }, + + /** + * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists + */ + supportsNativeGetElementsByClassName: function() { + return String(document.getElementsByClassName).indexOf("[native code]") !== -1; + }, + + /** + * As of now (19.04.2011) only supported by Firefox 4 and Chrome + * See https://developer.mozilla.org/en/DOM/Selection/modify + */ + supportsSelectionModify: function() { + return "getSelection" in window && "modify" in window.getSelection(); + }, + + /** + * Whether the browser supports the classList object for fast className manipulation + * See https://developer.mozilla.org/en/DOM/element.classList + */ + supportsClassList: function() { + return "classList" in testElement; + }, + + /** + * Opera needs a white space after a <br> in order to position the caret correctly + */ + needsSpaceAfterLineBreak: function() { + return isOpera; + }, + + /** + * Whether the browser supports the speech api on the given element + * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ + * + * @example + * var input = document.createElement("input"); + * if (wysihtml5.browser.supportsSpeechApiOn(input)) { + * // ... + * } + */ + supportsSpeechApiOn: function(input) { + var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0]; + return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input); + }, + + /** + * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest + * See https://connect.microsoft.com/ie/feedback/details/650112 + * or try the POC http://tifftiff.de/ie9_crash/ + */ + crashesWhenDefineProperty: function(property) { + return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest"); + }, + + /** + * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element + */ + doesAsyncFocus: function() { + return isIE; + }, + + /** + * In IE it's impssible for the user and for the selection library to set the caret after an <img> when it's the lastChild in the document + */ + hasProblemsSettingCaretAfterImg: function() { + return isIE; + }, + + hasUndoInContextMenu: function() { + return isGecko || isChrome || isOpera; + } + }; +})();wysihtml5.lang.array = function(arr) { + return { + /** + * Check whether a given object exists in an array + * + * @example + * wysihtml5.lang.array([1, 2]).contains(1); + * // => true + */ + contains: function(needle) { + if (arr.indexOf) { + return arr.indexOf(needle) !== -1; + } else { + for (var i=0, length=arr.length; i<length; i++) { + if (arr[i] === needle) { return true; } + } + return false; + } + }, + + /** + * Substract one array from another + * + * @example + * wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]); + * // => [1, 2] + */ + without: function(arrayToSubstract) { + arrayToSubstract = wysihtml5.lang.array(arrayToSubstract); + var newArr = [], + i = 0, + length = arr.length; + for (; i<length; i++) { + if (!arrayToSubstract.contains(arr[i])) { + newArr.push(arr[i]); + } + } + return newArr; + }, + + /** + * Return a clean native array + * + * Following will convert a Live NodeList to a proper Array + * @example + * var childNodes = wysihtml5.lang.array(document.body.childNodes).get(); + */ + get: function() { + var i = 0, + length = arr.length, + newArray = []; + for (; i<length; i++) { + newArray.push(arr[i]); + } + return newArray; + } + }; +};wysihtml5.lang.Dispatcher = Base.extend( + /** @scope wysihtml5.lang.Dialog.prototype */ { + observe: function(eventName, handler) { + this.events = this.events || {}; + this.events[eventName] = this.events[eventName] || []; + this.events[eventName].push(handler); + return this; + }, + + on: function() { + return this.observe.apply(this, wysihtml5.lang.array(arguments).get()); + }, + + fire: function(eventName, payload) { + this.events = this.events || {}; + var handlers = this.events[eventName] || [], + i = 0; + for (; i<handlers.length; i++) { + handlers[i].call(this, payload); + } + return this; + }, + + stopObserving: function(eventName, handler) { + this.events = this.events || {}; + var i = 0, + handlers, + newHandlers; + if (eventName) { + handlers = this.events[eventName] || [], + newHandlers = []; + for (; i<handlers.length; i++) { + if (handlers[i] !== handler && handler) { + newHandlers.push(handlers[i]); + } + } + this.events[eventName] = newHandlers; + } else { + // Clean up all events + this.events = {}; + } + return this; + } +});wysihtml5.lang.object = function(obj) { + return { + /** + * @example + * wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get(); + * // => { foo: 1, bar: 2, baz: 3 } + */ + merge: function(otherObj) { + for (var i in otherObj) { + obj[i] = otherObj[i]; + } + return this; + }, + + get: function() { + return obj; + }, + + /** + * @example + * wysihtml5.lang.object({ foo: 1 }).clone(); + * // => { foo: 1 } + */ + clone: function() { + var newObj = {}, + i; + for (i in obj) { + newObj[i] = obj[i]; + } + return newObj; + }, + + /** + * @example + * wysihtml5.lang.object([]).isArray(); + * // => true + */ + isArray: function() { + return Object.prototype.toString.call(obj) === "[object Array]"; + } + }; +};(function() { + var WHITE_SPACE_START = /^\s+/, + WHITE_SPACE_END = /\s+$/; + wysihtml5.lang.string = function(str) { + str = String(str); + return { + /** + * @example + * wysihtml5.lang.string(" foo ").trim(); + * // => "foo" + */ + trim: function() { + return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, ""); + }, + + /** + * @example + * wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" }); + * // => "Hello Christopher" + */ + interpolate: function(vars) { + for (var i in vars) { + str = this.replace("#{" + i + "}").by(vars[i]); + } + return str; + }, + + /** + * @example + * wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans"); + * // => "Hello Hans" + */ + replace: function(search) { + return { + by: function(replace) { + return str.split(search).join(replace); + } + } + } + }; + }; +})();/** + * Find urls in descendant text nodes of an element and auto-links them + * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/ + * + * @param {Element} element Container element in which to search for urls + * + * @example + * <div id="text-container">Please click here: www.google.com</div> + * <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script> + */ +(function(wysihtml5) { + var /** + * Don't auto-link urls that are contained in the following elements: + */ + IGNORE_URLS_IN = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]), + /** + * revision 1: + * /(\S+\.{1}[^\s\,\.\!]+)/g + * + * revision 2: + * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim + * + * put this in the beginning if you don't wan't to match within a word + * (^|[\>\(\{\[\s\>]) + */ + URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi, + TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i, + MAX_DISPLAY_LENGTH = 100, + BRACKETS = { ")": "(", "]": "[", "}": "{" }; + + function autoLink(element) { + if (_hasParentThatShouldBeIgnored(element)) { + return element; + } + + if (element === element.ownerDocument.documentElement) { + element = element.ownerDocument.body; + } + + return _parseNode(element); + } + + /** + * This is basically a rebuild of + * the rails auto_link_urls text helper + */ + function _convertUrlsToLinks(str) { + return str.replace(URL_REG_EXP, function(match, url) { + var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "", + opening = BRACKETS[punctuation]; + url = url.replace(TRAILING_CHAR_REG_EXP, ""); + + if (url.split(opening).length > url.split(punctuation).length) { + url = url + punctuation; + punctuation = ""; + } + var realUrl = url, + displayUrl = url; + if (url.length > MAX_DISPLAY_LENGTH) { + displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "..."; + } + // Add http prefix if necessary + if (realUrl.substr(0, 4) === "www.") { + realUrl = "http://" + realUrl; + } + + return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation; + }); + } + + /** + * Creates or (if already cached) returns a temp element + * for the given document object + */ + function _getTempElement(context) { + var tempElement = context._wysihtml5_tempElement; + if (!tempElement) { + tempElement = context._wysihtml5_tempElement = context.createElement("div"); + } + return tempElement; + } + + /** + * Replaces the original text nodes with the newly auto-linked dom tree + */ + function _wrapMatchesInNode(textNode) { + var parentNode = textNode.parentNode, + tempElement = _getTempElement(parentNode.ownerDocument); + + // We need to insert an empty/temporary <span /> to fix IE quirks + // Elsewise IE would strip white space in the beginning + tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(textNode.data); + tempElement.removeChild(tempElement.firstChild); + + while (tempElement.firstChild) { + // inserts tempElement.firstChild before textNode + parentNode.insertBefore(tempElement.firstChild, textNode); + } + parentNode.removeChild(textNode); + } + + function _hasParentThatShouldBeIgnored(node) { + var nodeName; + while (node.parentNode) { + node = node.parentNode; + nodeName = node.nodeName; + if (IGNORE_URLS_IN.contains(nodeName)) { + return true; + } else if (nodeName === "body") { + return false; + } + } + return false; + } + + function _parseNode(element) { + if (IGNORE_URLS_IN.contains(element.nodeName)) { + return; + } + + if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) { + _wrapMatchesInNode(element); + return; + } + + var childNodes = wysihtml5.lang.array(element.childNodes).get(), + childNodesLength = childNodes.length, + i = 0; + + for (; i<childNodesLength; i++) { + _parseNode(childNodes[i]); + } + + return element; + } + + wysihtml5.dom.autoLink = autoLink; + + // Reveal url reg exp to the outside + wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP; +})(wysihtml5);(function(wysihtml5) { + var supportsClassList = wysihtml5.browser.supportsClassList(), + api = wysihtml5.dom; + + api.addClass = function(element, className) { + if (supportsClassList) { + return element.classList.add(className); + } + if (api.hasClass(element, className)) { + return; + } + element.className += " " + className; + }; + + api.removeClass = function(element, className) { + if (supportsClassList) { + return element.classList.remove(className); + } + + element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " "); + }; + + api.hasClass = function(element, className) { + if (supportsClassList) { + return element.classList.contains(className); + } + + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }; +})(wysihtml5); +wysihtml5.dom.contains = (function() { + var documentElement = document.documentElement; + if (documentElement.contains) { + return function(container, element) { + if (element.nodeType !== wysihtml5.ELEMENT_NODE) { + element = element.parentNode; + } + return container !== element && container.contains(element); + }; + } else if (documentElement.compareDocumentPosition) { + return function(container, element) { + // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition + return !!(container.compareDocumentPosition(element) & 16); + }; + } +})();/** + * Converts an HTML fragment/element into a unordered/ordered list + * + * @param {Element} element The element which should be turned into a list + * @param {String} listType The list type in which to convert the tree (either "ul" or "ol") + * @return {Element} The created list + * + * @example + * <!-- Assume the following dom: --> + * <span id="pseudo-list"> + * eminem<br> + * dr. dre + * <div>50 Cent</div> + * </span> + * + * <script> + * wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul"); + * </script> + * + * <!-- Will result in: --> + * <ul> + * <li>eminem</li> + * <li>dr. dre</li> + * <li>50 Cent</li> + * </ul> + */ +wysihtml5.dom.convertToList = (function() { + function _createListItem(doc, list) { + var listItem = doc.createElement("li"); + list.appendChild(listItem); + return listItem; + } + + function _createList(doc, type) { + return doc.createElement(type); + } + + function convertToList(element, listType) { + if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") { + // Already a list + return element; + } + + var doc = element.ownerDocument, + list = _createList(doc, listType), + lineBreaks = element.querySelectorAll("br"), + lineBreaksLength = lineBreaks.length, + childNodes, + childNodesLength, + childNode, + lineBreak, + parentNode, + isBlockElement, + isLineBreak, + currentListItem, + i; + + // First find <br> at the end of inline elements and move them behind them + for (i=0; i<lineBreaksLength; i++) { + lineBreak = lineBreaks[i]; + while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) { + if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") { + parentNode.removeChild(lineBreak); + break; + } + wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode); + } + } + + childNodes = wysihtml5.lang.array(element.childNodes).get(); + childNodesLength = childNodes.length; + + for (i=0; i<childNodesLength; i++) { + currentListItem = currentListItem || _createListItem(doc, list); + childNode = childNodes[i]; + isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block"; + isLineBreak = childNode.nodeName === "BR"; + + if (isBlockElement) { + // Append blockElement to current <li> if empty, otherwise create a new one + currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem; + currentListItem.appendChild(childNode); + currentListItem = null; + continue; + } + + if (isLineBreak) { + // Only create a new list item in the next iteration when the current one has already content + currentListItem = currentListItem.firstChild ? null : currentListItem; + continue; + } + + currentListItem.appendChild(childNode); + } + + element.parentNode.replaceChild(list, element); + return list; + } + + return convertToList; +})();/** + * Copy a set of attributes from one element to another + * + * @param {Array} attributesToCopy List of attributes which should be copied + * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to + * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked + * with the element where to copy the attributes to (see example) + * + * @example + * var textarea = document.querySelector("textarea"), + * div = document.querySelector("div[contenteditable=true]"), + * anotherDiv = document.querySelector("div.preview"); + * wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv); + * + */ +wysihtml5.dom.copyAttributes = function(attributesToCopy) { + return { + from: function(elementToCopyFrom) { + return { + to: function(elementToCopyTo) { + var attribute, + i = 0, + length = attributesToCopy.length; + for (; i<length; i++) { + attribute = attributesToCopy[i]; + if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") { + elementToCopyTo[attribute] = elementToCopyFrom[attribute]; + } + } + return { andTo: arguments.callee }; + } + }; + } + }; +};/** + * Copy a set of styles from one element to another + * Please note that this only works properly across browsers when the element from which to copy the styles + * is in the dom + * + * Interesting article on how to copy styles + * + * @param {Array} stylesToCopy List of styles which should be copied + * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to + * copy the styles from., this again returns an object which provides a method named "to" which can be invoked + * with the element where to copy the styles to (see example) + * + * @example + * var textarea = document.querySelector("textarea"), + * div = document.querySelector("div[contenteditable=true]"), + * anotherDiv = document.querySelector("div.preview"); + * wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv); + * + */ +(function(dom) { + + /** + * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set + * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then + * its computed css width will be 198px + */ + var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"]; + + var shouldIgnoreBoxSizingBorderBox = function(element) { + if (hasBoxSizingBorderBox(element)) { + return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth; + } + return false; + }; + + var hasBoxSizingBorderBox = function(element) { + var i = 0, + length = BOX_SIZING_PROPERTIES.length; + for (; i<length; i++) { + if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") { + return BOX_SIZING_PROPERTIES[i]; + } + } + }; + + dom.copyStyles = function(stylesToCopy) { + return { + from: function(element) { + if (shouldIgnoreBoxSizingBorderBox(element)) { + stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES); + } + + var cssText = "", + length = stylesToCopy.length, + i = 0, + property; + for (; i<length; i++) { + property = stylesToCopy[i]; + cssText += property + ":" + dom.getStyle(property).from(element) + ";"; + } + + return { + to: function(element) { + dom.setStyles(cssText).on(element); + return { andTo: arguments.callee }; + } + }; + } + }; + }; +})(wysihtml5.dom);/** + * Event Delegation + * + * @example + * wysihtml5.dom.delegate(document.body, "a", "click", function() { + * // foo + * }); + */ +(function(wysihtml5) { + + wysihtml5.dom.delegate = function(container, selector, eventName, handler) { + return wysihtml5.dom.observe(container, eventName, function(event) { + var target = event.target, + match = wysihtml5.lang.array(container.querySelectorAll(selector)); + + while (target && target !== container) { + if (match.contains(target)) { + handler.call(target, event); + break; + } + target = target.parentNode; + } + }); + }; + +})(wysihtml5);/** + * Returns the given html wrapped in a div element + * + * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly + * when inserted via innerHTML + * + * @param {String} html The html which should be wrapped in a dom element + * @param {Obejct} [context] Document object of the context the html belongs to + * + * @example + * wysihtml5.dom.getAsDom("<article>foo</article>"); + */ +wysihtml5.dom.getAsDom = (function() { + + var _innerHTMLShiv = function(html, context) { + var tempElement = context.createElement("div"); + tempElement.style.display = "none"; + context.body.appendChild(tempElement); + // IE throws an exception when trying to insert <frameset></frameset> via innerHTML + try { tempElement.innerHTML = html; } catch(e) {} + context.body.removeChild(tempElement); + return tempElement; + }; + + /** + * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element + */ + var _ensureHTML5Compatibility = function(context) { + if (context._wysihtml5_supportsHTML5Tags) { + return; + } + for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) { + context.createElement(HTML5_ELEMENTS[i]); + } + context._wysihtml5_supportsHTML5Tags = true; + }; + + + /** + * List of html5 tags + * taken from http://simon.html5.org/html5-elements + */ + var HTML5_ELEMENTS = [ + "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption", + "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress", + "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr" + ]; + + return function(html, context) { + context = context || document; + var tempElement; + if (typeof(html) === "object" && html.nodeType) { + tempElement = context.createElement("div"); + tempElement.appendChild(html); + } else if (wysihtml5.browser.supportsHTML5Tags(context)) { + tempElement = context.createElement("div"); + tempElement.innerHTML = html; + } else { + _ensureHTML5Compatibility(context); + tempElement = _innerHTMLShiv(html, context); + } + return tempElement; + }; +})();/** + * Walks the dom tree from the given node up until it finds a match + * Designed for optimal performance. + * + * @param {Element} node The from which to check the parent nodes + * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp) + * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50) + * @return {null|Element} Returns the first element that matched the desiredNodeName(s) + * @example + * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] }); + * // ... or ... + * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" }); + * // ... or ... + * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g }); + */ +wysihtml5.dom.getParentElement = (function() { + + function _isSameNodeName(nodeName, desiredNodeNames) { + if (!desiredNodeNames || !desiredNodeNames.length) { + return true; + } + + if (typeof(desiredNodeNames) === "string") { + return nodeName === desiredNodeNames; + } else { + return wysihtml5.lang.array(desiredNodeNames).contains(nodeName); + } + } + + function _isElement(node) { + return node.nodeType === wysihtml5.ELEMENT_NODE; + } + + function _hasClassName(element, className, classRegExp) { + var classNames = (element.className || "").match(classRegExp) || []; + if (!className) { + return !!classNames.length; + } + return classNames[classNames.length - 1] === className; + } + + function _getParentElementWithNodeName(node, nodeName, levels) { + while (levels-- && node && node.nodeName !== "BODY") { + if (_isSameNodeName(node.nodeName, nodeName)) { + return node; + } + node = node.parentNode; + } + return null; + } + + function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) { + while (levels-- && node && node.nodeName !== "BODY") { + if (_isElement(node) && + _isSameNodeName(node.nodeName, nodeName) && + _hasClassName(node, className, classRegExp)) { + return node; + } + node = node.parentNode; + } + return null; + } + + return function(node, matchingSet, levels) { + levels = levels || 50; // Go max 50 nodes upwards from current node + if (matchingSet.className || matchingSet.classRegExp) { + return _getParentElementWithNodeNameAndClassName( + node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels + ); + } else { + return _getParentElementWithNodeName( + node, matchingSet.nodeName, levels + ); + } + }; +})(); +/** + * Get element's style for a specific css property + * + * @param {Element} element The element on which to retrieve the style + * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...) + * + * @example + * wysihtml5.dom.getStyle("display").from(document.body); + * // => "block" + */ +wysihtml5.dom.getStyle = (function() { + var stylePropertyMapping = { + "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat" + }, + REG_EXP_CAMELIZE = /\-[a-z]/g; + + function camelize(str) { + return str.replace(REG_EXP_CAMELIZE, function(match) { + return match.charAt(1).toUpperCase(); + }); + } + + return function(property) { + return { + from: function(element) { + if (element.nodeType !== wysihtml5.ELEMENT_NODE) { + return; + } + + var doc = element.ownerDocument, + camelizedProperty = stylePropertyMapping[property] || camelize(property), + style = element.style, + currentStyle = element.currentStyle, + styleValue = style[camelizedProperty]; + if (styleValue) { + return styleValue; + } + + // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant + // window.getComputedStyle, since it returns css property values in their original unit: + // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle + // gives you the original "50%". + // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio + if (currentStyle) { + try { + return currentStyle[camelizedProperty]; + } catch(e) { + //ie will occasionally fail for unknown reasons. swallowing exception + } + } + + var win = doc.defaultView || doc.parentWindow, + needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA", + originalOverflow, + returnValue; + + if (win.getComputedStyle) { + // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars + // therfore we remove and restore the scrollbar and calculate the value in between + if (needsOverflowReset) { + originalOverflow = style.overflow; + style.overflow = "hidden"; + } + returnValue = win.getComputedStyle(element, null).getPropertyValue(property); + if (needsOverflowReset) { + style.overflow = originalOverflow || ""; + } + return returnValue; + } + } + }; + }; +})();/** + * High performant way to check whether an element with a specific tag name is in the given document + * Optimized for being heavily executed + * Unleashes the power of live node lists + * + * @param {Object} doc The document object of the context where to check + * @param {String} tagName Upper cased tag name + * @example + * wysihtml5.dom.hasElementWithTagName(document, "IMG"); + */ +wysihtml5.dom.hasElementWithTagName = (function() { + var LIVE_CACHE = {}, + DOCUMENT_IDENTIFIER = 1; + + function _getDocumentIdentifier(doc) { + return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); + } + + return function(doc, tagName) { + var key = _getDocumentIdentifier(doc) + ":" + tagName, + cacheEntry = LIVE_CACHE[key]; + if (!cacheEntry) { + cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); + } + + return cacheEntry.length > 0; + }; +})();/** + * High performant way to check whether an element with a specific class name is in the given document + * Optimized for being heavily executed + * Unleashes the power of live node lists + * + * @param {Object} doc The document object of the context where to check + * @param {String} tagName Upper cased tag name + * @example + * wysihtml5.dom.hasElementWithClassName(document, "foobar"); + */ +(function(wysihtml5) { + var LIVE_CACHE = {}, + DOCUMENT_IDENTIFIER = 1; + + function _getDocumentIdentifier(doc) { + return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); + } + + wysihtml5.dom.hasElementWithClassName = function(doc, className) { + // getElementsByClassName is not supported by IE<9 + // but is sometimes mocked via library code (which then doesn't return live node lists) + if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) { + return !!doc.querySelector("." + className); + } + + var key = _getDocumentIdentifier(doc) + ":" + className, + cacheEntry = LIVE_CACHE[key]; + if (!cacheEntry) { + cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className); + } + + return cacheEntry.length > 0; + }; +})(wysihtml5); +wysihtml5.dom.insert = function(elementToInsert) { + return { + after: function(element) { + element.parentNode.insertBefore(elementToInsert, element.nextSibling); + }, + + before: function(element) { + element.parentNode.insertBefore(elementToInsert, element); + }, + + into: function(element) { + element.appendChild(elementToInsert); + } + }; +};wysihtml5.dom.insertCSS = function(rules) { + rules = rules.join("\n"); + + return { + into: function(doc) { + var head = doc.head || doc.getElementsByTagName("head")[0], + styleElement = doc.createElement("style"); + + styleElement.type = "text/css"; + + if (styleElement.styleSheet) { + styleElement.styleSheet.cssText = rules; + } else { + styleElement.appendChild(doc.createTextNode(rules)); + } + + if (head) { + head.appendChild(styleElement); + } + } + }; +};/** + * Method to set dom events + * + * @example + * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); + */ +wysihtml5.dom.observe = function(element, eventNames, handler) { + eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames; + + var handlerWrapper, + eventName, + i = 0, + length = eventNames.length; + + for (; i<length; i++) { + eventName = eventNames[i]; + if (element.addEventListener) { + element.addEventListener(eventName, handler, false); + } else { + handlerWrapper = function(event) { + if (!("target" in event)) { + event.target = event.srcElement; + } + event.preventDefault = event.preventDefault || function() { + this.returnValue = false; + }; + event.stopPropagation = event.stopPropagation || function() { + this.cancelBubble = true; + }; + handler.call(element, event); + }; + element.attachEvent("on" + eventName, handlerWrapper); + } + } + + return { + stop: function() { + var eventName, + i = 0, + length = eventNames.length; + for (; i<length; i++) { + eventName = eventNames[i]; + if (element.removeEventListener) { + element.removeEventListener(eventName, handler, false); + } else { + element.detachEvent("on" + eventName, handlerWrapper); + } + } + } + }; +}; +/** + * HTML Sanitizer + * Rewrites the HTML based on given rules + * + * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized + * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will + * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the + * desired substitution. + * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing + * + * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element. + * + * @example + * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>'; + * wysihtml5.dom.parse(userHTML, { + * tags { + * p: "div", // Rename p tags to div tags + * font: "span" // Rename font tags to span tags + * div: true, // Keep them, also possible (same result when passing: "div" or true) + * script: undefined // Remove script elements + * } + * }); + * // => <div><div><span>foo bar</span></div></div> + * + * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>'; + * wysihtml5.dom.parse(userHTML); + * // => '<span><span><span><span>I'm a table!</span></span></span></span>' + * + * var userHTML = '<div>foobar<br>foobar</div>'; + * wysihtml5.dom.parse(userHTML, { + * tags: { + * div: undefined, + * br: true + * } + * }); + * // => '' + * + * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>'; + * wysihtml5.dom.parse(userHTML, { + * classes: { + * red: 1, + * green: 1 + * }, + * tags: { + * div: { + * rename_tag: "p" + * } + * } + * }); + * // => '<p class="red">foo</p><p>bar</p>' + */ +wysihtml5.dom.parse = (function() { + + /** + * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML + * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the + * node isn't closed + * + * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML. + */ + var NODE_TYPE_MAPPING = { + "1": _handleElement, + "3": _handleText + }, + // Rename unknown tags to this + DEFAULT_NODE_NAME = "span", + WHITE_SPACE_REG_EXP = /\s+/, + defaultRules = { tags: {}, classes: {} }, + currentRules = {}; + + /** + * Iterates over all childs of the element, recreates them, appends them into a document fragment + * which later replaces the entire body content + */ + function parse(elementOrHtml, rules, context, cleanUp) { + wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get(); + + context = context || elementOrHtml.ownerDocument || document; + var fragment = context.createDocumentFragment(), + isString = typeof(elementOrHtml) === "string", + element, + newNode, + firstChild; + + if (isString) { + element = wysihtml5.dom.getAsDom(elementOrHtml, context); + } else { + element = elementOrHtml; + } + + while (element.firstChild) { + firstChild = element.firstChild; + element.removeChild(firstChild); + newNode = _convert(firstChild, cleanUp); + if (newNode) { + fragment.appendChild(newNode); + } + } + + // Clear element contents + element.innerHTML = ""; + + // Insert new DOM tree + element.appendChild(fragment); + + return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element; + } + + function _convert(oldNode, cleanUp) { + var oldNodeType = oldNode.nodeType, + oldChilds = oldNode.childNodes, + oldChildsLength = oldChilds.length, + newNode, + method = NODE_TYPE_MAPPING[oldNodeType], + i = 0; + + newNode = method && method(oldNode); + + if (!newNode) { + return null; + } + + for (i=0; i<oldChildsLength; i++) { + newChild = _convert(oldChilds[i], cleanUp); + if (newChild) { + newNode.appendChild(newChild); + } + } + + // Cleanup senseless <span> elements + if (cleanUp && + newNode.childNodes.length <= 1 && + newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME && + !newNode.attributes.length) { + return newNode.firstChild; + } + + return newNode; + } + + function _handleElement(oldNode) { + var rule, + newNode, + endTag, + tagRules = currentRules.tags, + nodeName = oldNode.nodeName.toLowerCase(), + scopeName = oldNode.scopeName; + + /** + * We already parsed that element + * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) + */ + if (oldNode._wysihtml5) { + return null; + } + oldNode._wysihtml5 = 1; + + if (oldNode.className === "wysihtml5-temp") { + return null; + } + + /** + * IE is the only browser who doesn't include the namespace in the + * nodeName, that's why we have to prepend it by ourselves + * scopeName is a proprietary IE feature + * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx + */ + if (scopeName && scopeName != "HTML") { + nodeName = scopeName + ":" + nodeName; + } + + /** + * Repair node + * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags + * A <p> doesn't need to be closed according HTML4-5 spec, we simply replace it with a <div> to preserve its content and layout + */ + if ("outerHTML" in oldNode) { + if (!wysihtml5.browser.autoClosesUnclosedTags() && + oldNode.nodeName === "P" && + oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") { + nodeName = "div"; + } + } + + if (nodeName in tagRules) { + rule = tagRules[nodeName]; + if (!rule || rule.remove) { + return null; + } + + rule = typeof(rule) === "string" ? { rename_tag: rule } : rule; + } else if (oldNode.firstChild) { + rule = { rename_tag: DEFAULT_NODE_NAME }; + } else { + // Remove empty unknown elements + return null; + } + + newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName); + _handleAttributes(oldNode, newNode, rule); + + oldNode = null; + return newNode; + } + + function _handleAttributes(oldNode, newNode, rule) { + var attributes = {}, // fresh new set of attributes to set on newNode + setClass = rule.set_class, // classes to set + addClass = rule.add_class, // add classes based on existing attributes + setAttributes = rule.set_attributes, // attributes to set on the current node + checkAttributes = rule.check_attributes, // check/convert values of attributes + allowedClasses = currentRules.classes, + i = 0, + classes = [], + newClasses = [], + newUniqueClasses = [], + oldClasses = [], + classesLength, + newClassesLength, + currentClass, + newClass, + attributeName, + newAttributeValue, + method; + + if (setAttributes) { + attributes = wysihtml5.lang.object(setAttributes).clone(); + } + + if (checkAttributes) { + for (attributeName in checkAttributes) { + method = attributeCheckMethods[checkAttributes[attributeName]]; + if (!method) { + continue; + } + newAttributeValue = method(_getAttribute(oldNode, attributeName)); + if (typeof(newAttributeValue) === "string") { + attributes[attributeName] = newAttributeValue; + } + } + } + + if (setClass) { + classes.push(setClass); + } + + if (addClass) { + for (attributeName in addClass) { + method = addClassMethods[addClass[attributeName]]; + if (!method) { + continue; + } + newClass = method(_getAttribute(oldNode, attributeName)); + if (typeof(newClass) === "string") { + classes.push(newClass); + } + } + } + + // make sure that wysihtml5 temp class doesn't get stripped out + allowedClasses["_wysihtml5-temp-placeholder"] = 1; + + // add old classes last + oldClasses = oldNode.getAttribute("class"); + if (oldClasses) { + classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); + } + classesLength = classes.length; + for (; i<classesLength; i++) { + currentClass = classes[i]; + if (allowedClasses[currentClass]) { + newClasses.push(currentClass); + } + } + + // remove duplicate entries and preserve class specificity + newClassesLength = newClasses.length; + while (newClassesLength--) { + currentClass = newClasses[newClassesLength]; + if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) { + newUniqueClasses.unshift(currentClass); + } + } + + if (newUniqueClasses.length) { + attributes["class"] = newUniqueClasses.join(" "); + } + + // set attributes on newNode + for (attributeName in attributes) { + // Setting attributes can cause a js error in IE under certain circumstances + // eg. on a <img> under https when it's new attribute value is non-https + // TODO: Investigate this further and check for smarter handling + try { + newNode.setAttribute(attributeName, attributes[attributeName]); + } catch(e) {} + } + + // IE8 sometimes loses the width/height attributes when those are set before the "src" + // so we make sure to set them again + if (attributes.src) { + if (typeof(attributes.width) !== "undefined") { + newNode.setAttribute("width", attributes.width); + } + if (typeof(attributes.height) !== "undefined") { + newNode.setAttribute("height", attributes.height); + } + } + } + + /** + * IE gives wrong results for hasAttribute/getAttribute, for example: + * var td = document.createElement("td"); + * td.getAttribute("rowspan"); // => "1" in IE + * + * Therefore we have to check the element's outerHTML for the attribute + */ + var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(); + function _getAttribute(node, attributeName) { + attributeName = attributeName.toLowerCase(); + var nodeName = node.nodeName; + if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) { + // Get 'src' attribute value via object property since this will always contain the + // full absolute url (http://...) + // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host + // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url) + return node.src; + } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { + // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML + var outerHTML = node.outerHTML.toLowerCase(), + // TODO: This might not work for attributes without value: <input disabled> + hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; + + return hasAttribute ? node.getAttribute(attributeName) : null; + } else{ + return node.getAttribute(attributeName); + } + } + + /** + * Check whether the given node is a proper loaded image + * FIXME: Returns undefined when unknown (Chrome, Safari) + */ + function _isLoadedImage(node) { + try { + return node.complete && !node.mozMatchesSelector(":-moz-broken"); + } catch(e) { + if (node.complete && node.readyState === "complete") { + return true; + } + } + } + + function _handleText(oldNode) { + return oldNode.ownerDocument.createTextNode(oldNode.data); + } + + + // ------------ attribute checks ------------ \\ + var attributeCheckMethods = { + url: (function() { + var REG_EXP = /^https?:\/\//i; + return function(attributeValue) { + if (!attributeValue || !attributeValue.match(REG_EXP)) { + return null; + } + return attributeValue.replace(REG_EXP, function(match) { + return match.toLowerCase(); + }); + }; + })(), + + alt: (function() { + var REG_EXP = /[^ a-z0-9_\-]/gi; + return function(attributeValue) { + if (!attributeValue) { + return ""; + } + return attributeValue.replace(REG_EXP, ""); + }; + })(), + + numbers: (function() { + var REG_EXP = /\D/g; + return function(attributeValue) { + attributeValue = (attributeValue || "").replace(REG_EXP, ""); + return attributeValue || null; + }; + })() + }; + + // ------------ class converter (converts an html attribute to a class name) ------------ \\ + var addClassMethods = { + align_img: (function() { + var mapping = { + left: "wysiwyg-float-left", + right: "wysiwyg-float-right" + }; + return function(attributeValue) { + return mapping[String(attributeValue).toLowerCase()]; + }; + })(), + + align_text: (function() { + var mapping = { + left: "wysiwyg-text-align-left", + right: "wysiwyg-text-align-right", + center: "wysiwyg-text-align-center", + justify: "wysiwyg-text-align-justify" + }; + return function(attributeValue) { + return mapping[String(attributeValue).toLowerCase()]; + }; + })(), + + clear_br: (function() { + var mapping = { + left: "wysiwyg-clear-left", + right: "wysiwyg-clear-right", + both: "wysiwyg-clear-both", + all: "wysiwyg-clear-both" + }; + return function(attributeValue) { + return mapping[String(attributeValue).toLowerCase()]; + }; + })(), + + size_font: (function() { + var mapping = { + "1": "wysiwyg-font-size-xx-small", + "2": "wysiwyg-font-size-small", + "3": "wysiwyg-font-size-medium", + "4": "wysiwyg-font-size-large", + "5": "wysiwyg-font-size-x-large", + "6": "wysiwyg-font-size-xx-large", + "7": "wysiwyg-font-size-xx-large", + "-": "wysiwyg-font-size-smaller", + "+": "wysiwyg-font-size-larger" + }; + return function(attributeValue) { + return mapping[String(attributeValue).charAt(0)]; + }; + })() + }; + + return parse; +})();/** + * Checks for empty text node childs and removes them + * + * @param {Element} node The element in which to cleanup + * @example + * wysihtml5.dom.removeEmptyTextNodes(element); + */ +wysihtml5.dom.removeEmptyTextNodes = function(node) { + var childNode, + childNodes = wysihtml5.lang.array(node.childNodes).get(), + childNodesLength = childNodes.length, + i = 0; + for (; i<childNodesLength; i++) { + childNode = childNodes[i]; + if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") { + childNode.parentNode.removeChild(childNode); + } + } +}; +/** + * Renames an element (eg. a <div> to a <p>) and keeps its childs + * + * @param {Element} element The list element which should be renamed + * @param {Element} newNodeName The desired tag name + * + * @example + * <!-- Assume the following dom: --> + * <ul id="list"> + * <li>eminem</li> + * <li>dr. dre</li> + * <li>50 Cent</li> + * </ul> + * + * <script> + * wysihtml5.dom.renameElement(document.getElementById("list"), "ol"); + * </script> + * + * <!-- Will result in: --> + * <ol> + * <li>eminem</li> + * <li>dr. dre</li> + * <li>50 Cent</li> + * </ol> + */ +wysihtml5.dom.renameElement = function(element, newNodeName) { + var newElement = element.ownerDocument.createElement(newNodeName), + firstChild; + while (firstChild = element.firstChild) { + newElement.appendChild(firstChild); + } + wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement); + element.parentNode.replaceChild(newElement, element); + return newElement; +};/** + * Takes an element, removes it and replaces it with it's childs + * + * @param {Object} node The node which to replace with it's child nodes + * @example + * <div id="foo"> + * <span>hello</span> + * </div> + * <script> + * // Remove #foo and replace with it's children + * wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo")); + * </script> + */ +wysihtml5.dom.replaceWithChildNodes = function(node) { + if (!node.parentNode) { + return; + } + + if (!node.firstChild) { + node.parentNode.removeChild(node); + return; + } + + var fragment = node.ownerDocument.createDocumentFragment(); + while (node.firstChild) { + fragment.appendChild(node.firstChild); + } + node.parentNode.replaceChild(fragment, node); + node = fragment = null; +}; +/** + * Unwraps an unordered/ordered list + * + * @param {Element} element The list element which should be unwrapped + * + * @example + * <!-- Assume the following dom: --> + * <ul id="list"> + * <li>eminem</li> + * <li>dr. dre</li> + * <li>50 Cent</li> + * </ul> + * + * <script> + * wysihtml5.dom.resolveList(document.getElementById("list")); + * </script> + * + * <!-- Will result in: --> + * eminem<br> + * dr. dre<br> + * 50 Cent<br> + */ +(function(dom) { + function _isBlockElement(node) { + return dom.getStyle("display").from(node) === "block"; + } + + function _isLineBreak(node) { + return node.nodeName === "BR"; + } + + function _appendLineBreak(element) { + var lineBreak = element.ownerDocument.createElement("br"); + element.appendChild(lineBreak); + } + + function resolveList(list) { + if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") { + return; + } + + var doc = list.ownerDocument, + fragment = doc.createDocumentFragment(), + previousSibling = list.previousElementSibling || list.previousSibling, + firstChild, + lastChild, + isLastChild, + shouldAppendLineBreak, + listItem; + + if (previousSibling && !_isBlockElement(previousSibling)) { + _appendLineBreak(fragment); + } + + while (listItem = list.firstChild) { + lastChild = listItem.lastChild; + while (firstChild = listItem.firstChild) { + isLastChild = firstChild === lastChild; + // This needs to be done before appending it to the fragment, as it otherwise will loose style information + shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild); + fragment.appendChild(firstChild); + if (shouldAppendLineBreak) { + _appendLineBreak(fragment); + } + } + + listItem.parentNode.removeChild(listItem); + } + list.parentNode.replaceChild(fragment, list); + } + + dom.resolveList = resolveList; +})(wysihtml5.dom);/** + * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way + * + * Browser Compatibility: + * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted" + * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...) + * + * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons: + * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'") + * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...) + * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire + * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe + * can do anything as if the sandbox attribute wasn't set + * + * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready + * @param {Object} [config] Optional parameters + * + * @example + * new wysihtml5.dom.Sandbox(function(sandbox) { + * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">'; + * }); + */ +(function(wysihtml5) { + var /** + * Default configuration + */ + doc = document, + /** + * Properties to unset/protect on the window object + */ + windowProperties = [ + "parent", "top", "opener", "frameElement", "frames", + "localStorage", "globalStorage", "sessionStorage", "indexedDB" + ], + /** + * Properties on the window object which are set to an empty function + */ + windowProperties2 = [ + "open", "close", "openDialog", "showModalDialog", + "alert", "confirm", "prompt", + "openDatabase", "postMessage", + "XMLHttpRequest", "XDomainRequest" + ], + /** + * Properties to unset/protect on the document object + */ + documentProperties = [ + "referrer", + "write", "open", "close" + ]; + + wysihtml5.dom.Sandbox = Base.extend( + /** @scope wysihtml5.dom.Sandbox.prototype */ { + + constructor: function(readyCallback, config) { + this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION; + this.config = wysihtml5.lang.object({}).merge(config).get(); + this.iframe = this._createIframe(); + }, + + insertInto: function(element) { + if (typeof(element) === "string") { + element = doc.getElementById(element); + } + + element.appendChild(this.iframe); + }, + + getIframe: function() { + return this.iframe; + }, + + getWindow: function() { + this._readyError(); + }, + + getDocument: function() { + this._readyError(); + }, + + destroy: function() { + var iframe = this.getIframe(); + iframe.parentNode.removeChild(iframe); + }, + + _readyError: function() { + throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet"); + }, + + /** + * Creates the sandbox iframe + * + * Some important notes: + * - We can't use HTML5 sandbox for now: + * setting it causes that the iframe's dom can't be accessed from the outside + * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom + * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired. + * In order to make this happen we need to set the "allow-scripts" flag. + * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all. + * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document) + * - IE needs to have the security="restricted" attribute set before the iframe is + * inserted into the dom tree + * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even + * though it supports it + * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore + * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely + * on the onreadystatechange event + */ + _createIframe: function() { + var that = this, + iframe = doc.createElement("iframe"); + iframe.className = "wysihtml5-sandbox"; + wysihtml5.dom.setAttributes({ + "security": "restricted", + "allowtransparency": "true", + "frameborder": 0, + "width": 0, + "height": 0, + "marginwidth": 0, + "marginheight": 0 + }).on(iframe); + + // Setting the src like this prevents ssl warnings in IE6 + if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) { + iframe.src = "javascript:'<html></html>'"; + } + + iframe.onload = function() { + iframe.onreadystatechange = iframe.onload = null; + that._onLoadIframe(iframe); + }; + + iframe.onreadystatechange = function() { + if (/loaded|complete/.test(iframe.readyState)) { + iframe.onreadystatechange = iframe.onload = null; + that._onLoadIframe(iframe); + } + }; + + return iframe; + }, + + /** + * Callback for when the iframe has finished loading + */ + _onLoadIframe: function(iframe) { + // don't resume when the iframe got unloaded (eg. by removing it from the dom) + if (!wysihtml5.dom.contains(doc.documentElement, iframe)) { + return; + } + + var that = this, + iframeWindow = iframe.contentWindow, + iframeDocument = iframe.contentWindow.document, + charset = doc.characterSet || doc.charset || "utf-8", + sandboxHtml = this._getHtml({ + charset: charset, + stylesheets: this.config.stylesheets + }); + + // Create the basic dom tree including proper DOCTYPE and charset + iframeDocument.open("text/html", "replace"); + iframeDocument.write(sandboxHtml); + iframeDocument.close(); + + this.getWindow = function() { return iframe.contentWindow; }; + this.getDocument = function() { return iframe.contentWindow.document; }; + + // Catch js errors and pass them to the parent's onerror event + // addEventListener("error") doesn't work properly in some browsers + // TODO: apparently this doesn't work in IE9! + iframeWindow.onerror = function(errorMessage, fileName, lineNumber) { + throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber); + }; + + if (!wysihtml5.browser.supportsSandboxedIframes()) { + // Unset a bunch of sensitive variables + // Please note: This isn't hack safe! + // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information + // IE is secure though, which is the most important thing, since IE is the only browser, who + // takes over scripts & styles into contentEditable elements when copied from external websites + // or applications (Microsoft Word, ...) + var i, length; + for (i=0, length=windowProperties.length; i<length; i++) { + this._unset(iframeWindow, windowProperties[i]); + } + for (i=0, length=windowProperties2.length; i<length; i++) { + this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION); + } + for (i=0, length=documentProperties.length; i<length; i++) { + this._unset(iframeDocument, documentProperties[i]); + } + // This doesn't work in Safari 5 + // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit + this._unset(iframeDocument, "cookie", "", true); + } + + this.loaded = true; + + // Trigger the callback + setTimeout(function() { that.callback(that); }, 0); + }, + + _getHtml: function(templateVars) { + var stylesheets = templateVars.stylesheets, + html = "", + i = 0, + length; + stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets; + if (stylesheets) { + length = stylesheets.length; + for (; i<length; i++) { + html += '<link rel="stylesheet" href="' + stylesheets[i] + '">'; + } + } + templateVars.stylesheets = html; + + return wysihtml5.lang.string( + '<!DOCTYPE html><html><head>' + + '<meta charset="#{charset}">#{stylesheets}</head>' + + '<body></body></html>' + ).interpolate(templateVars); + }, + + /** + * Method to unset/override existing variables + * @example + * // Make cookie unreadable and unwritable + * this._unset(document, "cookie", "", true); + */ + _unset: function(object, property, value, setter) { + try { object[property] = value; } catch(e) {} + + try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} + if (setter) { + try { object.__defineSetter__(property, function() {}); } catch(e) {} + } + + if (!wysihtml5.browser.crashesWhenDefineProperty(property)) { + try { + var config = { + get: function() { return value; } + }; + if (setter) { + config.set = function() {}; + } + Object.defineProperty(object, property, config); + } catch(e) {} + } + } + }); +})(wysihtml5); +(function() { + var mapping = { + "className": "class" + }; + wysihtml5.dom.setAttributes = function(attributes) { + return { + on: function(element) { + for (var i in attributes) { + element.setAttribute(mapping[i] || i, attributes[i]); + } + } + } + }; +})();wysihtml5.dom.setStyles = function(styles) { + return { + on: function(element) { + var style = element.style; + if (typeof(styles) === "string") { + style.cssText += ";" + styles; + return; + } + for (var i in styles) { + if (i === "float") { + style.cssFloat = styles[i]; + style.styleFloat = styles[i]; + } else { + style[i] = styles[i]; + } + } + } + }; +};/** + * Simulate HTML5 placeholder attribute + * + * Needed since + * - div[contentEditable] elements don't support it + * - older browsers (such as IE8 and Firefox 3.6) don't support it at all + * + * @param {Object} parent Instance of main wysihtml5.Editor class + * @param {Element} view Instance of wysihtml5.views.* class + * @param {String} placeholderText + * + * @example + * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar"); + */ +(function(dom) { + dom.simulatePlaceholder = function(editor, view, placeholderText) { + var CLASS_NAME = "placeholder", + unset = function() { + if (view.hasPlaceholderSet()) { + view.clear(); + } + dom.removeClass(view.element, CLASS_NAME); + }, + set = function() { + if (view.isEmpty()) { + view.setValue(placeholderText); + dom.addClass(view.element, CLASS_NAME); + } + }; + + editor + .observe("set_placeholder", set) + .observe("unset_placeholder", unset) + .observe("focus:composer", unset) + .observe("paste:composer", unset) + .observe("blur:composer", set); + + set(); + }; +})(wysihtml5.dom); +(function(dom) { + var documentElement = document.documentElement; + if ("textContent" in documentElement) { + dom.setTextContent = function(element, text) { + element.textContent = text; + }; + + dom.getTextContent = function(element) { + return element.textContent; + }; + } else if ("innerText" in documentElement) { + dom.setTextContent = function(element, text) { + element.innerText = text; + }; + + dom.getTextContent = function(element) { + return element.innerText; + }; + } else { + dom.setTextContent = function(element, text) { + element.nodeValue = text; + }; + + dom.getTextContent = function(element) { + return element.nodeValue; + }; + } +})(wysihtml5.dom); + +/** + * Fix most common html formatting misbehaviors of browsers implementation when inserting + * content via copy & paste contentEditable + * + * @author Christopher Blum + */ +wysihtml5.quirks.cleanPastedHTML = (function() { + // TODO: We probably need more rules here + var defaultRules = { + // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling + "a u": wysihtml5.dom.replaceWithChildNodes + }; + + function cleanPastedHTML(elementOrHtml, rules, context) { + rules = rules || defaultRules; + context = context || elementOrHtml.ownerDocument || document; + + var element, + isString = typeof(elementOrHtml) === "string", + method, + matches, + matchesLength, + i, + j = 0; + if (isString) { + element = wysihtml5.dom.getAsDom(elementOrHtml, context); + } else { + element = elementOrHtml; + } + + for (i in rules) { + matches = element.querySelectorAll(i); + method = rules[i]; + matchesLength = matches.length; + for (; j<matchesLength; j++) { + method(matches[j]); + } + } + + matches = elementOrHtml = rules = null; + + return isString ? element.innerHTML : element; + } + + return cleanPastedHTML; +})();/** + * IE and Opera leave an empty paragraph in the contentEditable element after clearing it + * + * @param {Object} contentEditableElement The contentEditable element to observe for clearing events + * @exaple + * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); + */ +(function(wysihtml5) { + var dom = wysihtml5.dom; + + wysihtml5.quirks.ensureProperClearing = (function() { + var clearIfNecessary = function(event) { + var element = this; + setTimeout(function() { + var innerHTML = element.innerHTML.toLowerCase(); + if (innerHTML == "<p> </p>" || + innerHTML == "<p> </p><p> </p>") { + element.innerHTML = ""; + } + }, 0); + }; + + return function(composer) { + dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); + }; + })(); + + + + /** + * In Opera when the caret is in the first and only item of a list (<ul><li>|</li></ul>) and the list is the first child of the contentEditable element, it's impossible to delete the list by hitting backspace + * + * @param {Object} contentEditableElement The contentEditable element to observe for clearing events + * @exaple + * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); + */ + wysihtml5.quirks.ensureProperClearingOfLists = (function() { + var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"]; + + var clearIfNecessary = function(element, contentEditableElement) { + if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) { + return; + } + + var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI }); + if (!list) { + return; + } + + var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild; + if (!listIsFirstChildOfContentEditable) { + return; + } + + var hasOnlyOneListItem = list.childNodes.length <= 1; + if (!hasOnlyOneListItem) { + return; + } + + var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true; + if (!onlyListItemIsEmpty) { + return; + } + + list.parentNode.removeChild(list); + }; + + return function(composer) { + dom.observe(composer.element, "keydown", function(event) { + if (event.keyCode !== wysihtml5.BACKSPACE_KEY) { + return; + } + + var element = composer.selection.getSelectedNode(); + clearIfNecessary(element, composer.element); + }); + }; + })(); + +})(wysihtml5); +// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 +// +// In Firefox this: +// var d = document.createElement("div"); +// d.innerHTML ='<a href="~"></a>'; +// d.innerHTML; +// will result in: +// <a href="%7E"></a> +// which is wrong +(function(wysihtml5) { + var TILDE_ESCAPED = "%7E"; + wysihtml5.quirks.getCorrectInnerHTML = function(element) { + var innerHTML = element.innerHTML; + if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { + return innerHTML; + } + + var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"), + url, + urlToSearch, + length, + i; + for (i=0, length=elementsWithTilde.length; i<length; i++) { + url = elementsWithTilde[i].href || elementsWithTilde[i].src; + urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED); + innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url); + } + return innerHTML; + }; +})(wysihtml5);/** + * Some browsers don't insert line breaks when hitting return in a contentEditable element + * - Opera & IE insert new <p> on return + * - Chrome & Safari insert new <div> on return + * - Firefox inserts <br> on return (yippie!) + * + * @param {Element} element + * + * @example + * wysihtml5.quirks.insertLineBreakOnReturn(element); + */ +(function(wysihtml5) { + var dom = wysihtml5.dom, + USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"], + LIST_TAGS = ["UL", "OL", "MENU"]; + + wysihtml5.quirks.insertLineBreakOnReturn = function(composer) { + function unwrap(selectedNode) { + var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2); + if (!parentElement) { + return; + } + + var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE); + dom.insert(invisibleSpace).before(parentElement); + dom.replaceWithChildNodes(parentElement); + composer.selection.selectNode(invisibleSpace); + } + + function keyDown(event) { + var keyCode = event.keyCode; + if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) { + return; + } + + var element = event.target, + selectedNode = composer.selection.getSelectedNode(), + blockElement = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4); + if (blockElement) { + // Some browsers create <p> elements after leaving a list + // check after keydown of backspace and return whether a <p> got inserted and unwrap it + if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) { + setTimeout(function() { + var selectedNode = composer.selection.getSelectedNode(), + list, + div; + if (!selectedNode) { + return; + } + + list = dom.getParentElement(selectedNode, { + nodeName: LIST_TAGS + }, 2); + + if (list) { + return; + } + + unwrap(selectedNode); + }, 0); + } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) { + setTimeout(function() { + unwrap(composer.selection.getSelectedNode()); + }, 0); + } + return; + } + + if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) { + composer.commands.exec("insertLineBreak"); + event.preventDefault(); + } + } + + // keypress doesn't fire when you hit backspace + dom.observe(composer.element.ownerDocument, "keydown", keyDown); + }; +})(wysihtml5);/** + * Force rerendering of a given element + * Needed to fix display misbehaviors of IE + * + * @param {Element} element The element object which needs to be rerendered + * @example + * wysihtml5.quirks.redraw(document.body); + */ +(function(wysihtml5) { + var CLASS_NAME = "wysihtml5-quirks-redraw"; + + wysihtml5.quirks.redraw = function(element) { + wysihtml5.dom.addClass(element, CLASS_NAME); + wysihtml5.dom.removeClass(element, CLASS_NAME); + + // Following hack is needed for firefox to make sure that image resize handles are properly removed + try { + var doc = element.ownerDocument; + doc.execCommand("italic", false, null); + doc.execCommand("italic", false, null); + } catch(e) {} + }; +})(wysihtml5);/** + * Selection API + * + * @example + * var selection = new wysihtml5.Selection(editor); + */ +(function(wysihtml5) { + var dom = wysihtml5.dom; + + function _getCumulativeOffsetTop(element) { + var top = 0; + if (element.parentNode) { + do { + top += element.offsetTop || 0; + element = element.offsetParent; + } while (element); + } + return top; + } + + wysihtml5.Selection = Base.extend( + /** @scope wysihtml5.Selection.prototype */ { + constructor: function(editor) { + // Make sure that our external range library is initialized + window.rangy.init(); + + this.editor = editor; + this.composer = editor.composer; + this.doc = this.composer.doc; + }, + + /** + * Get the current selection as a bookmark to be able to later restore it + * + * @return {Object} An object that represents the current selection + */ + getBookmark: function() { + var range = this.getRange(); + return range && range.cloneRange(); + }, + + /** + * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark + * + * @param {Object} bookmark An object that represents the current selection + */ + setBookmark: function(bookmark) { + if (!bookmark) { + return; + } + + this.setSelection(bookmark); + }, + + /** + * Set the caret in front of the given node + * + * @param {Object} node The element or text node where to position the caret in front of + * @example + * selection.setBefore(myElement); + */ + setBefore: function(node) { + var range = rangy.createRange(this.doc); + range.setStartBefore(node); + range.setEndBefore(node); + return this.setSelection(range); + }, + + /** + * Set the caret after the given node + * + * @param {Object} node The element or text node where to position the caret in front of + * @example + * selection.setBefore(myElement); + */ + setAfter: function(node) { + var range = rangy.createRange(this.doc); + range.setStartAfter(node); + range.setEndAfter(node); + return this.setSelection(range); + }, + + /** + * Ability to select/mark nodes + * + * @param {Element} node The node/element to select + * @example + * selection.selectNode(document.getElementById("my-image")); + */ + selectNode: function(node) { + var range = rangy.createRange(this.doc), + isElement = node.nodeType === wysihtml5.ELEMENT_NODE, + canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), + content = isElement ? node.innerHTML : node.data, + isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE), + displayStyle = dom.getStyle("display").from(node), + isBlockElement = (displayStyle === "block" || displayStyle === "list-item"); + + if (isEmpty && isElement && canHaveHTML) { + // Make sure that caret is visible in node by inserting a zero width no breaking space + try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} + } + + if (canHaveHTML) { + range.selectNodeContents(node); + } else { + range.selectNode(node); + } + + if (canHaveHTML && isEmpty && isElement) { + range.collapse(isBlockElement); + } else if (canHaveHTML && isEmpty) { + range.setStartAfter(node); + range.setEndAfter(node); + } + + this.setSelection(range); + }, + + /** + * Get the node which contains the selection + * + * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange" + * @return {Object} The node that contains the caret + * @example + * var nodeThatContainsCaret = selection.getSelectedNode(); + */ + getSelectedNode: function(controlRange) { + var selection, + range; + + if (controlRange && this.doc.selection && this.doc.selection.type === "Control") { + range = this.doc.selection.createRange(); + if (range && range.length) { + return range.item(0); + } + } + + selection = this.getSelection(this.doc); + if (selection.focusNode === selection.anchorNode) { + return selection.focusNode; + } else { + range = this.getRange(this.doc); + return range ? range.commonAncestorContainer : this.doc.body; + } + }, + + executeAndRestore: function(method, restoreScrollPosition) { + var body = this.doc.body, + oldScrollTop = restoreScrollPosition && body.scrollTop, + oldScrollLeft = restoreScrollPosition && body.scrollLeft, + className = "_wysihtml5-temp-placeholder", + placeholderHTML = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>', + range = this.getRange(this.doc), + newRange; + + // Nothing selected, execute and say goodbye + if (!range) { + method(body, body); + return; + } + + var node = range.createContextualFragment(placeholderHTML); + range.insertNode(node); + + // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder + try { + method(range.startContainer, range.endContainer); + } catch(e3) { + setTimeout(function() { throw e3; }, 0); + } + + caretPlaceholder = this.doc.querySelector("." + className); + if (caretPlaceholder) { + newRange = rangy.createRange(this.doc); + newRange.selectNode(caretPlaceholder); + newRange.deleteContents(); + this.setSelection(newRange); + } else { + // fallback for when all hell breaks loose + body.focus(); + } + + if (restoreScrollPosition) { + body.scrollTop = oldScrollTop; + body.scrollLeft = oldScrollLeft; + } + + // Remove it again, just to make sure that the placeholder is definitely out of the dom tree + try { + caretPlaceholder.parentNode.removeChild(caretPlaceholder); + } catch(e4) {} + }, + + /** + * Different approach of preserving the selection (doesn't modify the dom) + * Takes all text nodes in the selection and saves the selection position in the first and last one + */ + executeAndRestoreSimple: function(method) { + var range = this.getRange(), + body = this.doc.body, + newRange, + firstNode, + lastNode, + textNodes, + rangeBackup; + + // Nothing selected, execute and say goodbye + if (!range) { + method(body, body); + return; + } + + textNodes = range.getNodes([3]); + firstNode = textNodes[0] || range.startContainer; + lastNode = textNodes[textNodes.length - 1] || range.endContainer; + + rangeBackup = { + collapsed: range.collapsed, + startContainer: firstNode, + startOffset: firstNode === range.startContainer ? range.startOffset : 0, + endContainer: lastNode, + endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length + }; + + try { + method(range.startContainer, range.endContainer); + } catch(e) { + setTimeout(function() { throw e; }, 0); + } + + newRange = rangy.createRange(this.doc); + try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {} + try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {} + try { this.setSelection(newRange); } catch(e3) {} + }, + + /** + * Insert html at the caret position and move the cursor after the inserted html + * + * @param {String} html HTML string to insert + * @example + * selection.insertHTML("<p>foobar</p>"); + */ + insertHTML: function(html) { + var range = rangy.createRange(this.doc), + node = range.createContextualFragment(html), + lastChild = node.lastChild; + this.insertNode(node); + if (lastChild) { + this.setAfter(lastChild); + } + }, + + /** + * Insert a node at the caret position and move the cursor behind it + * + * @param {Object} node HTML string to insert + * @example + * selection.insertNode(document.createTextNode("foobar")); + */ + insertNode: function(node) { + var range = this.getRange(); + if (range) { + range.insertNode(node); + } + }, + + /** + * Wraps current selection with the given node + * + * @param {Object} node The node to surround the selected elements with + */ + surround: function(node) { + var range = this.getRange(); + if (!range) { + return; + } + + try { + // This only works when the range boundaries are not overlapping other elements + range.surroundContents(node); + this.selectNode(node); + } catch(e) { + // fallback + node.appendChild(range.extractContents()); + range.insertNode(node); + } + }, + + /** + * Scroll the current caret position into the view + * FIXME: This is a bit hacky, there might be a smarter way of doing this + * + * @example + * selection.scrollIntoView(); + */ + scrollIntoView: function() { + var doc = this.doc, + hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, + tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() { + var element = doc.createElement("span"); + // The element needs content in order to be able to calculate it's position properly + element.innerHTML = wysihtml5.INVISIBLE_SPACE; + return element; + })(), + offsetTop; + + if (hasScrollBars) { + this.insertNode(tempElement); + offsetTop = _getCumulativeOffsetTop(tempElement); + tempElement.parentNode.removeChild(tempElement); + if (offsetTop > doc.body.scrollTop) { + doc.body.scrollTop = offsetTop; + } + } + }, + + /** + * Select line where the caret is in + */ + selectLine: function() { + if (wysihtml5.browser.supportsSelectionModify()) { + this._selectLine_W3C(); + } else if (this.doc.selection) { + this._selectLine_MSIE(); + } + }, + + /** + * See https://developer.mozilla.org/en/DOM/Selection/modify + */ + _selectLine_W3C: function() { + var win = this.doc.defaultView, + selection = win.getSelection(); + selection.modify("extend", "left", "lineboundary"); + selection.modify("extend", "right", "lineboundary"); + }, + + _selectLine_MSIE: function() { + var range = this.doc.selection.createRange(), + rangeTop = range.boundingTop, + rangeHeight = range.boundingHeight, + scrollWidth = this.doc.body.scrollWidth, + rangeBottom, + rangeEnd, + measureNode, + i, + j; + + if (!range.moveToPoint) { + return; + } + + if (rangeTop === 0) { + // Don't know why, but when the selection ends at the end of a line + // range.boundingTop is 0 + measureNode = this.doc.createElement("span"); + this.insertNode(measureNode); + rangeTop = measureNode.offsetTop; + measureNode.parentNode.removeChild(measureNode); + } + + rangeTop += 1; + + for (i=-10; i<scrollWidth; i+=2) { + try { + range.moveToPoint(i, rangeTop); + break; + } catch(e1) {} + } + + // Investigate the following in order to handle multi line selections + // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0); + rangeBottom = rangeTop; + rangeEnd = this.doc.selection.createRange(); + for (j=scrollWidth; j>=0; j--) { + try { + rangeEnd.moveToPoint(j, rangeBottom); + break; + } catch(e2) {} + } + + range.setEndPoint("EndToEnd", rangeEnd); + range.select(); + }, + + getText: function() { + var selection = this.getSelection(); + return selection ? selection.toString() : ""; + }, + + getNodes: function(nodeType, filter) { + var range = this.getRange(); + if (range) { + return range.getNodes([nodeType], filter); + } else { + return []; + } + }, + + getRange: function() { + var selection = this.getSelection(); + return selection && selection.rangeCount && selection.getRangeAt(0); + }, + + getSelection: function() { + return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow); + }, + + setSelection: function(range) { + var win = this.doc.defaultView || this.doc.parentWindow, + selection = rangy.getSelection(win); + return selection.setSingleRange(range); + } + }); + +})(wysihtml5); +/** + * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license. + * http://code.google.com/p/rangy/ + * + * changed in order to be able ... + * - to use custom tags + * - to detect and replace similar css classes via reg exp + */ +(function(wysihtml5, rangy) { + var defaultTagName = "span"; + + var REG_EXP_WHITE_SPACE = /\s+/g; + + function hasClass(el, cssClass, regExp) { + if (!el.className) { + return false; + } + + var matchingClassNames = el.className.match(regExp) || []; + return matchingClassNames[matchingClassNames.length - 1] === cssClass; + } + + function addClass(el, cssClass, regExp) { + if (el.className) { + removeClass(el, regExp); + el.className += " " + cssClass; + } else { + el.className = cssClass; + } + } + + function removeClass(el, regExp) { + if (el.className) { + el.className = el.className.replace(regExp, ""); + } + } + + function hasSameClasses(el1, el2) { + return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " "); + } + + function replaceWithOwnChildren(el) { + var parent = el.parentNode; + while (el.firstChild) { + parent.insertBefore(el.firstChild, el); + } + parent.removeChild(el); + } + + function elementsHaveSameNonClassAttributes(el1, el2) { + if (el1.attributes.length != el2.attributes.length) { + return false; + } + for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { + attr1 = el1.attributes[i]; + name = attr1.name; + if (name != "class") { + attr2 = el2.attributes.getNamedItem(name); + if (attr1.specified != attr2.specified) { + return false; + } + if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) { + return false; + } + } + } + return true; + } + + function isSplitPoint(node, offset) { + if (rangy.dom.isCharacterDataNode(node)) { + if (offset == 0) { + return !!node.previousSibling; + } else if (offset == node.length) { + return !!node.nextSibling; + } else { + return true; + } + } + + return offset > 0 && offset < node.childNodes.length; + } + + function splitNodeAt(node, descendantNode, descendantOffset) { + var newNode; + if (rangy.dom.isCharacterDataNode(descendantNode)) { + if (descendantOffset == 0) { + descendantOffset = rangy.dom.getNodeIndex(descendantNode); + descendantNode = descendantNode.parentNode; + } else if (descendantOffset == descendantNode.length) { + descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1; + descendantNode = descendantNode.parentNode; + } else { + newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset); + } + } + if (!newNode) { + newNode = descendantNode.cloneNode(false); + if (newNode.id) { + newNode.removeAttribute("id"); + } + var child; + while ((child = descendantNode.childNodes[descendantOffset])) { + newNode.appendChild(child); + } + rangy.dom.insertAfter(newNode, descendantNode); + } + return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode)); + } + + function Merge(firstNode) { + this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE); + this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; + this.textNodes = [this.firstTextNode]; + } + + Merge.prototype = { + doMerge: function() { + var textBits = [], textNode, parent, text; + for (var i = 0, len = this.textNodes.length; i < len; ++i) { + textNode = this.textNodes[i]; + parent = textNode.parentNode; + textBits[i] = textNode.data; + if (i) { + parent.removeChild(textNode); + if (!parent.hasChildNodes()) { + parent.parentNode.removeChild(parent); + } + } + } + this.firstTextNode.data = text = textBits.join(""); + return text; + }, + + getLength: function() { + var i = this.textNodes.length, len = 0; + while (i--) { + len += this.textNodes[i].length; + } + return len; + }, + + toString: function() { + var textBits = []; + for (var i = 0, len = this.textNodes.length; i < len; ++i) { + textBits[i] = "'" + this.textNodes[i].data + "'"; + } + return "[Merge(" + textBits.join(",") + ")]"; + } + }; + + function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) { + this.tagNames = tagNames || [defaultTagName]; + this.cssClass = cssClass || ""; + this.similarClassRegExp = similarClassRegExp; + this.normalize = normalize; + this.applyToAnyTagName = false; + } + + HTMLApplier.prototype = { + getAncestorWithClass: function(node) { + var cssClassMatch; + while (node) { + cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true; + if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) { + return node; + } + node = node.parentNode; + } + return false; + }, + + // Normalizes nodes after applying a CSS class to a Range. + postApply: function(textNodes, range) { + var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; + + var merges = [], currentMerge; + + var rangeStartNode = firstNode, rangeEndNode = lastNode; + var rangeStartOffset = 0, rangeEndOffset = lastNode.length; + + var textNode, precedingTextNode; + + for (var i = 0, len = textNodes.length; i < len; ++i) { + textNode = textNodes[i]; + precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false); + if (precedingTextNode) { + if (!currentMerge) { + currentMerge = new Merge(precedingTextNode); + merges.push(currentMerge); + } + currentMerge.textNodes.push(textNode); + if (textNode === firstNode) { + rangeStartNode = currentMerge.firstTextNode; + rangeStartOffset = rangeStartNode.length; + } + if (textNode === lastNode) { + rangeEndNode = currentMerge.firstTextNode; + rangeEndOffset = currentMerge.getLength(); + } + } else { + currentMerge = null; + } + } + + // Test whether the first node after the range needs merging + var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true); + if (nextTextNode) { + if (!currentMerge) { + currentMerge = new Merge(lastNode); + merges.push(currentMerge); + } + currentMerge.textNodes.push(nextTextNode); + } + + // Do the merges + if (merges.length) { + for (i = 0, len = merges.length; i < len; ++i) { + merges[i].doMerge(); + } + // Set the range boundaries + range.setStart(rangeStartNode, rangeStartOffset); + range.setEnd(rangeEndNode, rangeEndOffset); + } + }, + + getAdjacentMergeableTextNode: function(node, forward) { + var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE); + var el = isTextNode ? node.parentNode : node; + var adjacentNode; + var propName = forward ? "nextSibling" : "previousSibling"; + if (isTextNode) { + // Can merge if the node's previous/next sibling is a text node + adjacentNode = node[propName]; + if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) { + return adjacentNode; + } + } else { + // Compare element with its sibling + adjacentNode = el[propName]; + if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) { + return adjacentNode[forward ? "firstChild" : "lastChild"]; + } + } + return null; + }, + + areElementsMergeable: function(el1, el2) { + return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase()) + && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase()) + && hasSameClasses(el1, el2) + && elementsHaveSameNonClassAttributes(el1, el2); + }, + + createContainer: function(doc) { + var el = doc.createElement(this.tagNames[0]); + if (this.cssClass) { + el.className = this.cssClass; + } + return el; + }, + + applyToTextNode: function(textNode) { + var parent = textNode.parentNode; + if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { + if (this.cssClass) { + addClass(parent, this.cssClass, this.similarClassRegExp); + } + } else { + var el = this.createContainer(rangy.dom.getDocument(textNode)); + textNode.parentNode.insertBefore(el, textNode); + el.appendChild(textNode); + } + }, + + isRemovable: function(el) { + return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass; + }, + + undoToTextNode: function(textNode, range, ancestorWithClass) { + if (!range.containsNode(ancestorWithClass)) { + // Split out the portion of the ancestor from which we can remove the CSS class + var ancestorRange = range.cloneRange(); + ancestorRange.selectNode(ancestorWithClass); + + if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) { + splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset); + range.setEndAfter(ancestorWithClass); + } + if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) { + ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset); + } + } + + if (this.similarClassRegExp) { + removeClass(ancestorWithClass, this.similarClassRegExp); + } + if (this.isRemovable(ancestorWithClass)) { + replaceWithOwnChildren(ancestorWithClass); + } + }, + + applyToRange: function(range) { + var textNodes = range.getNodes([wysihtml5.TEXT_NODE]); + if (!textNodes.length) { + try { + var node = this.createContainer(range.endContainer.ownerDocument); + range.surroundContents(node); + this.selectNode(range, node); + return; + } catch(e) {} + } + + range.splitBoundaries(); + textNodes = range.getNodes([wysihtml5.TEXT_NODE]); + + if (textNodes.length) { + var textNode; + + for (var i = 0, len = textNodes.length; i < len; ++i) { + textNode = textNodes[i]; + if (!this.getAncestorWithClass(textNode)) { + this.applyToTextNode(textNode); + } + } + + range.setStart(textNodes[0], 0); + textNode = textNodes[textNodes.length - 1]; + range.setEnd(textNode, textNode.length); + + if (this.normalize) { + this.postApply(textNodes, range); + } + } + }, + + undoToRange: function(range) { + var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass; + if (textNodes.length) { + range.splitBoundaries(); + textNodes = range.getNodes([wysihtml5.TEXT_NODE]); + } else { + var doc = range.endContainer.ownerDocument, + node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + range.insertNode(node); + range.selectNode(node); + textNodes = [node]; + } + + for (var i = 0, len = textNodes.length; i < len; ++i) { + textNode = textNodes[i]; + ancestorWithClass = this.getAncestorWithClass(textNode); + if (ancestorWithClass) { + this.undoToTextNode(textNode, range, ancestorWithClass); + } + } + + if (len == 1) { + this.selectNode(range, textNodes[0]); + } else { + range.setStart(textNodes[0], 0); + textNode = textNodes[textNodes.length - 1]; + range.setEnd(textNode, textNode.length); + + if (this.normalize) { + this.postApply(textNodes, range); + } + } + }, + + selectNode: function(range, node) { + var isElement = node.nodeType === wysihtml5.ELEMENT_NODE, + canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true, + content = isElement ? node.innerHTML : node.data, + isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE); + + if (isEmpty && isElement && canHaveHTML) { + // Make sure that caret is visible in node by inserting a zero width no breaking space + try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} + } + range.selectNodeContents(node); + if (isEmpty && isElement) { + range.collapse(false); + } else if (isEmpty) { + range.setStartAfter(node); + range.setEndAfter(node); + } + }, + + getTextSelectedByRange: function(textNode, range) { + var textRange = range.cloneRange(); + textRange.selectNodeContents(textNode); + + var intersectionRange = textRange.intersection(range); + var text = intersectionRange ? intersectionRange.toString() : ""; + textRange.detach(); + + return text; + }, + + isAppliedToRange: function(range) { + var ancestors = [], + ancestor, + textNodes = range.getNodes([wysihtml5.TEXT_NODE]); + if (!textNodes.length) { + ancestor = this.getAncestorWithClass(range.startContainer); + return ancestor ? [ancestor] : false; + } + + for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) { + selectedText = this.getTextSelectedByRange(textNodes[i], range); + ancestor = this.getAncestorWithClass(textNodes[i]); + if (selectedText != "" && !ancestor) { + return false; + } else { + ancestors.push(ancestor); + } + } + return ancestors; + }, + + toggleRange: function(range) { + if (this.isAppliedToRange(range)) { + this.undoToRange(range); + } else { + this.applyToRange(range); + } + } + }; + + wysihtml5.selection.HTMLApplier = HTMLApplier; + +})(wysihtml5, rangy);/** + * Rich Text Query/Formatting Commands + * + * @example + * var commands = new wysihtml5.Commands(editor); + */ +wysihtml5.Commands = Base.extend( + /** @scope wysihtml5.Commands.prototype */ { + constructor: function(editor) { + this.editor = editor; + this.composer = editor.composer; + this.doc = this.composer.doc; + }, + + /** + * Check whether the browser supports the given command + * + * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") + * @example + * commands.supports("createLink"); + */ + support: function(command) { + return wysihtml5.browser.supportsCommand(this.doc, command); + }, + + /** + * Check whether the browser supports the given command + * + * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList") + * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...) + * @example + * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg"); + */ + exec: function(command, value) { + var obj = wysihtml5.commands[command], + args = wysihtml5.lang.array(arguments).get(), + method = obj && obj.exec, + result = null; + + this.editor.fire("beforecommand:composer"); + + if (method) { + args.unshift(this.composer); + result = method.apply(obj, args); + } else { + try { + // try/catch for buggy firefox + result = this.doc.execCommand(command, false, value); + } catch(e) {} + } + + this.editor.fire("aftercommand:composer"); + return result; + }, + + /** + * Check whether the current command is active + * If the caret is within a bold text, then calling this with command "bold" should return true + * + * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") + * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src) + * @return {Boolean} Whether the command is active + * @example + * var isCurrentSelectionBold = commands.state("bold"); + */ + state: function(command, commandValue) { + var obj = wysihtml5.commands[command], + args = wysihtml5.lang.array(arguments).get(), + method = obj && obj.state; + if (method) { + args.unshift(this.composer); + return method.apply(obj, args); + } else { + try { + // try/catch for buggy firefox + return this.doc.queryCommandState(command); + } catch(e) { + return false; + } + } + }, + + /** + * Get the current command's value + * + * @param {String} command The command string which to check (eg. "formatBlock") + * @return {String} The command value + * @example + * var currentBlockElement = commands.value("formatBlock"); + */ + value: function(command) { + var obj = wysihtml5.commands[command], + method = obj && obj.value; + if (method) { + return method.call(obj, this.composer, command); + } else { + try { + // try/catch for buggy firefox + return this.doc.queryCommandValue(command); + } catch(e) { + return null; + } + } + } +}); +(function(wysihtml5) { + var undef; + + wysihtml5.commands.bold = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, "b"); + }, + + state: function(composer, command, color) { + // element.ownerDocument.queryCommandState("bold") results: + // firefox: only <b> + // chrome: <b>, <strong>, <h1>, <h2>, ... + // ie: <b>, <strong> + // opera: <b>, <strong> + return wysihtml5.commands.formatInline.state(composer, command, "b"); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5); + +(function(wysihtml5) { + var undef, + NODE_NAME = "A", + dom = wysihtml5.dom; + + function _removeFormat(composer, anchors) { + var length = anchors.length, + i = 0, + anchor, + codeElement, + textContent; + for (; i<length; i++) { + anchor = anchors[i]; + codeElement = dom.getParentElement(anchor, { nodeName: "code" }); + textContent = dom.getTextContent(anchor); + + // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking + // else replace <a> with its childNodes + if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) { + // <code> element is used to prevent later auto-linking of the content + codeElement = dom.renameElement(anchor, "code"); + } else { + dom.replaceWithChildNodes(anchor); + } + } + } + + function _format(composer, attributes) { + var doc = composer.doc, + tempClass = "_wysihtml5-temp-" + (+new Date()), + tempClassRegExp = /non-matching-class/g, + i = 0, + length, + anchors, + anchor, + hasElementChild, + isEmpty, + elementToSetCaretAfter, + textContent, + whiteSpace, + j; + wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp); + anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass); + length = anchors.length; + for (; i<length; i++) { + anchor = anchors[i]; + anchor.removeAttribute("class"); + for (j in attributes) { + anchor.setAttribute(j, attributes[j]); + } + } + + elementToSetCaretAfter = anchor; + if (length === 1) { + textContent = dom.getTextContent(anchor); + hasElementChild = !!anchor.querySelector("*"); + isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE; + if (!hasElementChild && isEmpty) { + dom.setTextContent(anchor, attributes.text || anchor.href); + whiteSpace = doc.createTextNode(" "); + composer.selection.setAfter(anchor); + composer.selection.insertNode(whiteSpace); + elementToSetCaretAfter = whiteSpace; + } + } + composer.selection.setAfter(elementToSetCaretAfter); + } + + wysihtml5.commands.createLink = { + /** + * TODO: Use HTMLApplier or formatInline here + * + * Turns selection into a link + * If selection is already a link, it removes the link and wraps it with a <code> element + * The <code> element is needed to avoid auto linking + * + * @example + * // either ... + * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de"); + * // ... or ... + * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" }); + */ + exec: function(composer, command, value) { + var anchors = this.state(composer, command); + if (anchors) { + // Selection contains links + composer.selection.executeAndRestore(function() { + _removeFormat(composer, anchors); + }); + } else { + // Create links + value = typeof(value) === "object" ? value : { href: value }; + _format(composer, value); + } + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, "A"); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);/** + * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags + * which we don't want + * Instead we set a css class + */ +(function(wysihtml5) { + var undef, + REG_EXP = /wysiwyg-font-size-[a-z\-]+/g; + + wysihtml5.commands.fontSize = { + exec: function(composer, command, size) { + return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); + }, + + state: function(composer, command, size) { + return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5); +/** + * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags + * which we don't want + * Instead we set a css class + */ +(function(wysihtml5) { + var undef, + REG_EXP = /wysiwyg-color-[a-z]+/g; + + wysihtml5.commands.foreColor = { + exec: function(composer, command, color) { + return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); + }, + + state: function(composer, command, color) { + return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef, + dom = wysihtml5.dom, + DEFAULT_NODE_NAME = "DIV", + // Following elements are grouped + // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4 + // instead of creating a H4 within a H1 which would result in semantically invalid html + BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME]; + + /** + * Remove similiar classes (based on classRegExp) + * and add the desired class name + */ + function _addClass(element, className, classRegExp) { + if (element.className) { + _removeClass(element, classRegExp); + element.className += " " + className; + } else { + element.className = className; + } + } + + function _removeClass(element, classRegExp) { + element.className = element.className.replace(classRegExp, ""); + } + + /** + * Check whether given node is a text node and whether it's empty + */ + function _isBlankTextNode(node) { + return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim(); + } + + /** + * Returns previous sibling node that is not a blank text node + */ + function _getPreviousSiblingThatIsNotBlank(node) { + var previousSibling = node.previousSibling; + while (previousSibling && _isBlankTextNode(previousSibling)) { + previousSibling = previousSibling.previousSibling; + } + return previousSibling; + } + + /** + * Returns next sibling node that is not a blank text node + */ + function _getNextSiblingThatIsNotBlank(node) { + var nextSibling = node.nextSibling; + while (nextSibling && _isBlankTextNode(nextSibling)) { + nextSibling = nextSibling.nextSibling; + } + return nextSibling; + } + + /** + * Adds line breaks before and after the given node if the previous and next siblings + * aren't already causing a visual line break (block element or <br>) + */ + function _addLineBreakBeforeAndAfter(node) { + var doc = node.ownerDocument, + nextSibling = _getNextSiblingThatIsNotBlank(node), + previousSibling = _getPreviousSiblingThatIsNotBlank(node); + + if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) { + node.parentNode.insertBefore(doc.createElement("br"), nextSibling); + } + if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) { + node.parentNode.insertBefore(doc.createElement("br"), node); + } + } + + /** + * Removes line breaks before and after the given node + */ + function _removeLineBreakBeforeAndAfter(node) { + var nextSibling = _getNextSiblingThatIsNotBlank(node), + previousSibling = _getPreviousSiblingThatIsNotBlank(node); + + if (nextSibling && _isLineBreak(nextSibling)) { + nextSibling.parentNode.removeChild(nextSibling); + } + if (previousSibling && _isLineBreak(previousSibling)) { + previousSibling.parentNode.removeChild(previousSibling); + } + } + + function _removeLastChildIfLineBreak(node) { + var lastChild = node.lastChild; + if (lastChild && _isLineBreak(lastChild)) { + lastChild.parentNode.removeChild(lastChild); + } + } + + function _isLineBreak(node) { + return node.nodeName === "BR"; + } + + /** + * Checks whether the elment causes a visual line break + * (<br> or block elements) + */ + function _isLineBreakOrBlockElement(element) { + if (_isLineBreak(element)) { + return true; + } + + if (dom.getStyle("display").from(element) === "block") { + return true; + } + + return false; + } + + /** + * Execute native query command + * and if necessary modify the inserted node's className + */ + function _execCommand(doc, command, nodeName, className) { + if (className) { + var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) { + var target = event.target, + displayStyle; + if (target.nodeType !== wysihtml5.ELEMENT_NODE) { + return; + } + displayStyle = dom.getStyle("display").from(target); + if (displayStyle.substr(0, 6) !== "inline") { + // Make sure that only block elements receive the given class + target.className += " " + className; + } + }); + } + doc.execCommand(command, false, nodeName); + if (eventListener) { + eventListener.stop(); + } + } + + function _selectLineAndWrap(composer, element) { + composer.selection.selectLine(); + composer.selection.surround(element); + _removeLineBreakBeforeAndAfter(element); + _removeLastChildIfLineBreak(element); + composer.selection.selectNode(element); + } + + function _hasClasses(element) { + return !!wysihtml5.lang.string(element.className).trim(); + } + + wysihtml5.commands.formatBlock = { + exec: function(composer, command, nodeName, className, classRegExp) { + var doc = composer.doc, + blockElement = this.state(composer, command, nodeName, className, classRegExp), + selectedNode; + + nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; + + if (blockElement) { + composer.selection.executeAndRestoreSimple(function() { + if (classRegExp) { + _removeClass(blockElement, classRegExp); + } + var hasClasses = _hasClasses(blockElement); + if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) { + // Insert a line break afterwards and beforewards when there are siblings + // that are not of type line break or block element + _addLineBreakBeforeAndAfter(blockElement); + dom.replaceWithChildNodes(blockElement); + } else if (hasClasses) { + // Make sure that styling is kept by renaming the element to <div> and copying over the class name + dom.renameElement(blockElement, DEFAULT_NODE_NAME); + } + }); + return; + } + + // Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>) + if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) { + selectedNode = composer.selection.getSelectedNode(); + blockElement = dom.getParentElement(selectedNode, { + nodeName: BLOCK_ELEMENTS_GROUP + }); + + if (blockElement) { + composer.selection.executeAndRestoreSimple(function() { + // Rename current block element to new block element and add class + if (nodeName) { + blockElement = dom.renameElement(blockElement, nodeName); + } + if (className) { + _addClass(blockElement, className, classRegExp); + } + }); + return; + } + } + + if (composer.commands.support(command)) { + _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className); + return; + } + + blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME); + if (className) { + blockElement.className = className; + } + _selectLineAndWrap(composer, blockElement); + }, + + state: function(composer, command, nodeName, className, classRegExp) { + nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; + var selectedNode = composer.selection.getSelectedNode(); + return dom.getParentElement(selectedNode, { + nodeName: nodeName, + className: className, + classRegExp: classRegExp + }); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);/** + * formatInline scenarios for tag "B" (| = caret, |foo| = selected text) + * + * #1 caret in unformatted text: + * abcdefg| + * output: + * abcdefg<b>|</b> + * + * #2 unformatted text selected: + * abc|deg|h + * output: + * abc<b>|deg|</b>h + * + * #3 unformatted text selected across boundaries: + * ab|c <span>defg|h</span> + * output: + * ab<b>|c </b><span><b>defg</b>|h</span> + * + * #4 formatted text entirely selected + * <b>|abc|</b> + * output: + * |abc| + * + * #5 formatted text partially selected + * <b>ab|c|</b> + * output: + * <b>ab</b>|c| + * + * #6 formatted text selected across boundaries + * <span>ab|c</span> <b>de|fgh</b> + * output: + * <span>ab|c</span> de|<b>fgh</b> + */ +(function(wysihtml5) { + var undef, + // Treat <b> as <strong> and vice versa + ALIAS_MAPPING = { + "strong": "b", + "em": "i", + "b": "strong", + "i": "em" + }, + htmlApplier = {}; + + function _getTagNames(tagName) { + var alias = ALIAS_MAPPING[tagName]; + return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()]; + } + + function _getApplier(tagName, className, classRegExp) { + var identifier = tagName + ":" + className; + if (!htmlApplier[identifier]) { + htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true); + } + return htmlApplier[identifier]; + } + + wysihtml5.commands.formatInline = { + exec: function(composer, command, tagName, className, classRegExp) { + var range = composer.selection.getRange(); + if (!range) { + return false; + } + _getApplier(tagName, className, classRegExp).toggleRange(range); + composer.selection.setSelection(range); + }, + + state: function(composer, command, tagName, className, classRegExp) { + var doc = composer.doc, + aliasTagName = ALIAS_MAPPING[tagName] || tagName, + range; + + // Check whether the document contains a node with the desired tagName + if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) && + !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) { + return false; + } + + // Check whether the document contains a node with the desired className + if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) { + return false; + } + + range = composer.selection.getRange(); + if (!range) { + return false; + } + + return _getApplier(tagName, className, classRegExp).isAppliedToRange(range); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef; + + wysihtml5.commands.insertHTML = { + exec: function(composer, command, html) { + if (composer.commands.support(command)) { + composer.doc.execCommand(command, false, html); + } else { + composer.selection.insertHTML(html); + } + }, + + state: function() { + return false; + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var NODE_NAME = "IMG"; + + wysihtml5.commands.insertImage = { + /** + * Inserts an <img> + * If selection is already an image link, it removes it + * + * @example + * // either ... + * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg"); + * // ... or ... + * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" }); + */ + exec: function(composer, command, value) { + value = typeof(value) === "object" ? value : { src: value }; + + var doc = composer.doc, + image = this.state(composer), + textNode, + i, + parent; + + if (image) { + // Image already selected, set the caret before it and delete it + composer.selection.setBefore(image); + parent = image.parentNode; + parent.removeChild(image); + + // and it's parent <a> too if it hasn't got any other relevant child nodes + wysihtml5.dom.removeEmptyTextNodes(parent); + if (parent.nodeName === "A" && !parent.firstChild) { + composer.selection.setAfter(parent); + parent.parentNode.removeChild(parent); + } + + // firefox and ie sometimes don't remove the image handles, even though the image got removed + wysihtml5.quirks.redraw(composer.element); + return; + } + + image = doc.createElement(NODE_NAME); + + for (i in value) { + image[i] = value[i]; + } + + composer.selection.insertNode(image); + if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) { + textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + composer.selection.insertNode(textNode); + composer.selection.setAfter(textNode); + } else { + composer.selection.setAfter(image); + } + }, + + state: function(composer) { + var doc = composer.doc, + selectedNode, + text, + imagesInSelection; + + if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) { + return false; + } + + selectedNode = composer.selection.getSelectedNode(); + if (!selectedNode) { + return false; + } + + if (selectedNode.nodeName === NODE_NAME) { + // This works perfectly in IE + return selectedNode; + } + + if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) { + return false; + } + + text = composer.selection.getText(); + text = wysihtml5.lang.string(text).trim(); + if (text) { + return false; + } + + imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) { + return node.nodeName === "IMG"; + }); + + if (imagesInSelection.length !== 1) { + return false; + } + + return imagesInSelection[0]; + }, + + value: function(composer) { + var image = this.state(composer); + return image && image.src; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef, + LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); + + wysihtml5.commands.insertLineBreak = { + exec: function(composer, command) { + if (composer.commands.support(command)) { + composer.doc.execCommand(command, false, null); + if (!wysihtml5.browser.autoScrollsToCaret()) { + composer.selection.scrollIntoView(); + } + } else { + composer.commands.exec("insertHTML", LINE_BREAK); + } + }, + + state: function() { + return false; + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef; + + wysihtml5.commands.insertOrderedList = { + exec: function(composer, command) { + var doc = composer.doc, + selectedNode = composer.selection.getSelectedNode(), + list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), + otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), + tempClassName = "_wysihtml5-temp-" + new Date().getTime(), + isEmpty, + tempElement; + + if (composer.commands.support(command)) { + doc.execCommand(command, false, null); + return; + } + + if (list) { + // Unwrap list + // <ol><li>foo</li><li>bar</li></ol> + // becomes: + // foo<br>bar<br> + composer.selection.executeAndRestoreSimple(function() { + wysihtml5.dom.resolveList(list); + }); + } else if (otherList) { + // Turn an unordered list into an ordered list + // <ul><li>foo</li><li>bar</li></ul> + // becomes: + // <ol><li>foo</li><li>bar</li></ol> + composer.selection.executeAndRestoreSimple(function() { + wysihtml5.dom.renameElement(otherList, "ol"); + }); + } else { + // Create list + composer.commands.exec("formatBlock", "div", tempClassName); + tempElement = doc.querySelector("." + tempClassName); + isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; + composer.selection.executeAndRestoreSimple(function() { + list = wysihtml5.dom.convertToList(tempElement, "ol"); + }); + if (isEmpty) { + composer.selection.selectNode(list.querySelector("li")); + } + } + }, + + state: function(composer) { + var selectedNode = composer.selection.getSelectedNode(); + return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef; + + wysihtml5.commands.insertUnorderedList = { + exec: function(composer, command) { + var doc = composer.doc, + selectedNode = composer.selection.getSelectedNode(), + list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), + otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), + tempClassName = "_wysihtml5-temp-" + new Date().getTime(), + isEmpty, + tempElement; + + if (composer.commands.support(command)) { + doc.execCommand(command, false, null); + return; + } + + if (list) { + // Unwrap list + // <ul><li>foo</li><li>bar</li></ul> + // becomes: + // foo<br>bar<br> + composer.selection.executeAndRestoreSimple(function() { + wysihtml5.dom.resolveList(list); + }); + } else if (otherList) { + // Turn an ordered list into an unordered list + // <ol><li>foo</li><li>bar</li></ol> + // becomes: + // <ul><li>foo</li><li>bar</li></ul> + composer.selection.executeAndRestoreSimple(function() { + wysihtml5.dom.renameElement(otherList, "ul"); + }); + } else { + // Create list + composer.commands.exec("formatBlock", "div", tempClassName); + tempElement = doc.querySelector("." + tempClassName); + isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; + composer.selection.executeAndRestoreSimple(function() { + list = wysihtml5.dom.convertToList(tempElement, "ul"); + }); + if (isEmpty) { + composer.selection.selectNode(list.querySelector("li")); + } + } + }, + + state: function(composer) { + var selectedNode = composer.selection.getSelectedNode(); + return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef; + + wysihtml5.commands.italic = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, "i"); + }, + + state: function(composer, command, color) { + // element.ownerDocument.queryCommandState("italic") results: + // firefox: only <i> + // chrome: <i>, <em>, <blockquote>, ... + // ie: <i>, <em> + // opera: only <i> + return wysihtml5.commands.formatInline.state(composer, command, "i"); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef, + CLASS_NAME = "wysiwyg-text-align-center", + REG_EXP = /wysiwyg-text-align-[a-z]+/g; + + wysihtml5.commands.justifyCenter = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef, + CLASS_NAME = "wysiwyg-text-align-left", + REG_EXP = /wysiwyg-text-align-[a-z]+/g; + + wysihtml5.commands.justifyLeft = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef, + CLASS_NAME = "wysiwyg-text-align-right", + REG_EXP = /wysiwyg-text-align-[a-z]+/g; + + wysihtml5.commands.justifyRight = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);(function(wysihtml5) { + var undef; + wysihtml5.commands.underline = { + exec: function(composer, command) { + return wysihtml5.commands.formatInline.exec(composer, command, "u"); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, "u"); + }, + + value: function() { + return undef; + } + }; +})(wysihtml5);/** + * Undo Manager for wysihtml5 + * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface + */ +(function(wysihtml5) { + var Z_KEY = 90, + Y_KEY = 89, + BACKSPACE_KEY = 8, + DELETE_KEY = 46, + MAX_HISTORY_ENTRIES = 40, + UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', + REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', + dom = wysihtml5.dom; + + function cleanTempElements(doc) { + var tempElement; + while (tempElement = doc.querySelector("._wysihtml5-temp")) { + tempElement.parentNode.removeChild(tempElement); + } + } + + wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend( + /** @scope wysihtml5.UndoManager.prototype */ { + constructor: function(editor) { + this.editor = editor; + this.composer = editor.composer; + this.element = this.composer.element; + this.history = [this.composer.getValue()]; + this.position = 1; + + // Undo manager currently only supported in browsers who have the insertHTML command (not IE) + if (this.composer.commands.support("insertHTML")) { + this._observe(); + } + }, + + _observe: function() { + var that = this, + doc = this.composer.sandbox.getDocument(), + lastKey; + + // Catch CTRL+Z and CTRL+Y + dom.observe(this.element, "keydown", function(event) { + if (event.altKey || (!event.ctrlKey && !event.metaKey)) { + return; + } + + var keyCode = event.keyCode, + isUndo = keyCode === Z_KEY && !event.shiftKey, + isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY); + + if (isUndo) { + that.undo(); + event.preventDefault(); + } else if (isRedo) { + that.redo(); + event.preventDefault(); + } + }); + + // Catch delete and backspace + dom.observe(this.element, "keydown", function(event) { + var keyCode = event.keyCode; + if (keyCode === lastKey) { + return; + } + + lastKey = keyCode; + + if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) { + that.transact(); + } + }); + + // Now this is very hacky: + // These days browsers don't offer a undo/redo event which we could hook into + // to be notified when the user hits undo/redo in the contextmenu. + // Therefore we simply insert two elements as soon as the contextmenu gets opened. + // The last element being inserted will be immediately be removed again by a exexCommand("undo") + // => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu + // => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu + if (wysihtml5.browser.hasUndoInContextMenu()) { + var interval, observed, cleanUp = function() { + cleanTempElements(doc); + clearInterval(interval); + }; + + dom.observe(this.element, "contextmenu", function() { + cleanUp(); + that.composer.selection.executeAndRestoreSimple(function() { + if (that.element.lastChild) { + that.composer.selection.setAfter(that.element.lastChild); + } + + // enable undo button in context menu + doc.execCommand("insertHTML", false, UNDO_HTML); + // enable redo button in context menu + doc.execCommand("insertHTML", false, REDO_HTML); + doc.execCommand("undo", false, null); + }); + + interval = setInterval(function() { + if (doc.getElementById("_wysihtml5-redo")) { + cleanUp(); + that.redo(); + } else if (!doc.getElementById("_wysihtml5-undo")) { + cleanUp(); + that.undo(); + } + }, 400); + + if (!observed) { + observed = true; + dom.observe(document, "mousedown", cleanUp); + dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp); + } + }); + } + + this.editor + .observe("newword:composer", function() { + that.transact(); + }) + + .observe("beforecommand:composer", function() { + that.transact(); + }); + }, + + transact: function() { + var previousHtml = this.history[this.position - 1], + currentHtml = this.composer.getValue(); + + if (currentHtml == previousHtml) { + return; + } + + var length = this.history.length = this.position; + if (length > MAX_HISTORY_ENTRIES) { + this.history.shift(); + this.position--; + } + + this.position++; + this.history.push(currentHtml); + }, + + undo: function() { + this.transact(); + + if (this.position <= 1) { + return; + } + + this.set(this.history[--this.position - 1]); + this.editor.fire("undo:composer"); + }, + + redo: function() { + if (this.position >= this.history.length) { + return; + } + + this.set(this.history[++this.position - 1]); + this.editor.fire("redo:composer"); + }, + + set: function(html) { + this.composer.setValue(html); + this.editor.focus(true); + } + }); +})(wysihtml5); +/** + * TODO: the following methods still need unit test coverage + */ +wysihtml5.views.View = Base.extend( + /** @scope wysihtml5.views.View.prototype */ { + constructor: function(parent, textareaElement, config) { + this.parent = parent; + this.element = textareaElement; + this.config = config; + + this._observeViewChange(); + }, + + _observeViewChange: function() { + var that = this; + this.parent.observe("beforeload", function() { + that.parent.observe("change_view", function(view) { + if (view === that.name) { + that.parent.currentView = that; + that.show(); + // Using tiny delay here to make sure that the placeholder is set before focusing + setTimeout(function() { that.focus(); }, 0); + } else { + that.hide(); + } + }); + }); + }, + + focus: function() { + if (this.element.ownerDocument.querySelector(":focus") === this.element) { + return; + } + + try { this.element.focus(); } catch(e) {} + }, + + hide: function() { + this.element.style.display = "none"; + }, + + show: function() { + this.element.style.display = ""; + }, + + disable: function() { + this.element.setAttribute("disabled", "disabled"); + }, + + enable: function() { + this.element.removeAttribute("disabled"); + } +});(function(wysihtml5) { + var dom = wysihtml5.dom, + browser = wysihtml5.browser; + + wysihtml5.views.Composer = wysihtml5.views.View.extend( + /** @scope wysihtml5.views.Composer.prototype */ { + name: "composer", + + // Needed for firefox in order to display a proper caret in an empty contentEditable + CARET_HACK: "<br>", + + constructor: function(parent, textareaElement, config) { + this.base(parent, textareaElement, config); + this.textarea = this.parent.textarea; + this._initSandbox(); + }, + + clear: function() { + this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK; + }, + + getValue: function(parse) { + var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element); + + if (parse) { + value = this.parent.parse(value); + } + + // Replace all "zero width no breaking space" chars + // which are used as hacks to enable some functionalities + // Also remove all CARET hacks that somehow got left + value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by(""); + + return value; + }, + + setValue: function(html, parse) { + if (parse) { + html = this.parent.parse(html); + } + this.element.innerHTML = html; + }, + + show: function() { + this.iframe.style.display = this._displayStyle || ""; + + // Firefox needs this, otherwise contentEditable becomes uneditable + this.disable(); + this.enable(); + }, + + hide: function() { + this._displayStyle = dom.getStyle("display").from(this.iframe); + if (this._displayStyle === "none") { + this._displayStyle = null; + } + this.iframe.style.display = "none"; + }, + + disable: function() { + this.element.removeAttribute("contentEditable"); + this.base(); + }, + + enable: function() { + this.element.setAttribute("contentEditable", "true"); + this.base(); + }, + + focus: function(setToEnd) { + // IE 8 fires the focus event after .focus() + // This is needed by our simulate_placeholder.js to work + // therefore we clear it ourselves this time + if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) { + this.clear(); + } + + this.base(); + + var lastChild = this.element.lastChild; + if (setToEnd && lastChild) { + if (lastChild.nodeName === "BR") { + this.selection.setBefore(this.element.lastChild); + } else { + this.selection.setAfter(this.element.lastChild); + } + } + }, + + getTextContent: function() { + return dom.getTextContent(this.element); + }, + + hasPlaceholderSet: function() { + return this.getTextContent() == this.textarea.element.getAttribute("placeholder"); + }, + + isEmpty: function() { + var innerHTML = this.element.innerHTML, + elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea"; + return innerHTML === "" || + innerHTML === this.CARET_HACK || + this.hasPlaceholderSet() || + (this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue)); + }, + + _initSandbox: function() { + var that = this; + + this.sandbox = new dom.Sandbox(function() { + that._create(); + }, { + stylesheets: this.config.stylesheets + }); + this.iframe = this.sandbox.getIframe(); + + // Create hidden field which tells the server after submit, that the user used an wysiwyg editor + var hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "_wysihtml5_mode"; + hiddenField.value = 1; + + // Store reference to current wysihtml5 instance on the textarea element + var textareaElement = this.textarea.element; + dom.insert(this.iframe).after(textareaElement); + dom.insert(hiddenField).after(textareaElement); + }, + + _create: function() { + var that = this; + + this.doc = this.sandbox.getDocument(); + this.element = this.doc.body; + this.textarea = this.parent.textarea; + this.element.innerHTML = this.textarea.getValue(true); + this.enable(); + + // Make sure our selection handler is ready + this.selection = new wysihtml5.Selection(this.parent); + + // Make sure commands dispatcher is ready + this.commands = new wysihtml5.Commands(this.parent); + + dom.copyAttributes([ + "className", "spellcheck", "title", "lang", "dir", "accessKey" + ]).from(this.textarea.element).to(this.element); + + dom.addClass(this.element, this.config.composerClassName); + + // Make the editor look like the original textarea, by syncing styles + if (this.config.style) { + this.style(); + } + + this.observe(); + + var name = this.config.name; + if (name) { + dom.addClass(this.element, name); + dom.addClass(this.iframe, name); + } + + // Simulate html5 placeholder attribute on contentEditable element + var placeholderText = typeof(this.config.placeholder) === "string" + ? this.config.placeholder + : this.textarea.element.getAttribute("placeholder"); + if (placeholderText) { + dom.simulatePlaceholder(this.parent, this, placeholderText); + } + + // Make sure that the browser avoids using inline styles whenever possible + this.commands.exec("styleWithCSS", false); + + this._initAutoLinking(); + this._initObjectResizing(); + this._initUndoManager(); + + // Simulate html5 autofocus on contentEditable element + if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) { + setTimeout(function() { that.focus(); }, 100); + } + + wysihtml5.quirks.insertLineBreakOnReturn(this); + + // IE sometimes leaves a single paragraph, which can't be removed by the user + if (!browser.clearsContentEditableCorrectly()) { + wysihtml5.quirks.ensureProperClearing(this); + } + + if (!browser.clearsListsInContentEditableCorrectly()) { + wysihtml5.quirks.ensureProperClearingOfLists(this); + } + + // Set up a sync that makes sure that textarea and editor have the same content + if (this.initSync && this.config.sync) { + this.initSync(); + } + + // Okay hide the textarea, we are ready to go + this.textarea.hide(); + + // Fire global (before-)load event + this.parent.fire("beforeload").fire("load"); + }, + + _initAutoLinking: function() { + var that = this, + supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(), + supportsAutoLinking = browser.doesAutoLinkingInContentEditable(); + if (supportsDisablingOfAutoLinking) { + this.commands.exec("autoUrlDetect", false); + } + + if (!this.config.autoLink) { + return; + } + + // Only do the auto linking by ourselves when the browser doesn't support auto linking + // OR when he supports auto linking but we were able to turn it off (IE9+) + if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) { + this.parent.observe("newword:composer", function() { + that.selection.executeAndRestore(function(startContainer, endContainer) { + dom.autoLink(endContainer.parentNode); + }); + }); + } + + // Assuming we have the following: + // <a href="http://www.google.de">http://www.google.de</a> + // If a user now changes the url in the innerHTML we want to make sure that + // it's synchronized with the href attribute (as long as the innerHTML is still a url) + var // Use a live NodeList to check whether there are any links in the document + links = this.sandbox.getDocument().getElementsByTagName("a"), + // The autoLink helper method reveals a reg exp to detect correct urls + urlRegExp = dom.autoLink.URL_REG_EXP, + getTextContent = function(element) { + var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim(); + if (textContent.substr(0, 4) === "www.") { + textContent = "http://" + textContent; + } + return textContent; + }; + + dom.observe(this.element, "keydown", function(event) { + if (!links.length) { + return; + } + + var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument), + link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4), + textContent; + + if (!link) { + return; + } + + textContent = getTextContent(link); + // keydown is fired before the actual content is changed + // therefore we set a timeout to change the href + setTimeout(function() { + var newTextContent = getTextContent(link); + if (newTextContent === textContent) { + return; + } + + // Only set href when new href looks like a valid url + if (newTextContent.match(urlRegExp)) { + link.setAttribute("href", newTextContent); + } + }, 0); + }); + }, + + _initObjectResizing: function() { + var properties = ["width", "height"], + propertiesLength = properties.length, + element = this.element; + + this.commands.exec("enableObjectResizing", this.config.allowObjectResizing); + + if (this.config.allowObjectResizing) { + // IE sets inline styles after resizing objects + // The following lines make sure that the width/height css properties + // are copied over to the width/height attributes + if (browser.supportsEvent("resizeend")) { + dom.observe(element, "resizeend", function(event) { + var target = event.target || event.srcElement, + style = target.style, + i = 0, + property; + for(; i<propertiesLength; i++) { + property = properties[i]; + if (style[property]) { + target.setAttribute(property, parseInt(style[property], 10)); + style[property] = ""; + } + } + // After resizing IE sometimes forgets to remove the old resize handles + wysihtml5.quirks.redraw(element); + }); + } + } else { + if (browser.supportsEvent("resizestart")) { + dom.observe(element, "resizestart", function(event) { event.preventDefault(); }); + } + } + }, + + _initUndoManager: function() { + new wysihtml5.UndoManager(this.parent); + } + }); +})(wysihtml5);(function(wysihtml5) { + var dom = wysihtml5.dom, + doc = document, + win = window, + HOST_TEMPLATE = doc.createElement("div"), + /** + * Styles to copy from textarea to the composer element + */ + TEXT_FORMATTING = [ + "background-color", + "color", "cursor", + "font-family", "font-size", "font-style", "font-variant", "font-weight", + "line-height", "letter-spacing", + "text-align", "text-decoration", "text-indent", "text-rendering", + "word-break", "word-wrap", "word-spacing" + ], + /** + * Styles to copy from textarea to the iframe + */ + BOX_FORMATTING = [ + "background-color", + "border-collapse", + "border-bottom-color", "border-bottom-style", "border-bottom-width", + "border-left-color", "border-left-style", "border-left-width", + "border-right-color", "border-right-style", "border-right-width", + "border-top-color", "border-top-style", "border-top-width", + "clear", "display", "float", + "margin-bottom", "margin-left", "margin-right", "margin-top", + "outline-color", "outline-offset", "outline-width", "outline-style", + "padding-left", "padding-right", "padding-top", "padding-bottom", + "position", "top", "left", "right", "bottom", "z-index", + "vertical-align", "text-align", + "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing", + "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow", + "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius", + "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius", + "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius", + "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius", + "width", "height" + ], + /** + * Styles to sync while the window gets resized + */ + RESIZE_STYLE = [ + "width", "height", + "top", "left", "right", "bottom" + ], + ADDITIONAL_CSS_RULES = [ + "html { height: 100%; }", + "body { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }", + "._wysihtml5-temp { display: none; }", + wysihtml5.browser.isGecko ? + "body.placeholder { color: graytext !important; }" : + "body.placeholder { color: #a9a9a9 !important; }", + "body[disabled] { background-color: #eee !important; color: #999 !important; cursor: default !important; }", + // Ensure that user see's broken images and can delete them + "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" + ]; + + /** + * With "setActive" IE offers a smart way of focusing elements without scrolling them into view: + * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx + * + * Other browsers need a more hacky way: (pssst don't tell my mama) + * In order to prevent the element being scrolled into view when focusing it, we simply + * move it out of the scrollable area, focus it, and reset it's position + */ + var focusWithoutScrolling = function(element) { + if (element.setActive) { + // Following line could cause a js error when the textarea is invisible + // See https://github.com/xing/wysihtml5/issues/9 + try { element.setActive(); } catch(e) {} + } else { + var elementStyle = element.style, + originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, + originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft, + originalStyles = { + position: elementStyle.position, + top: elementStyle.top, + left: elementStyle.left, + WebkitUserSelect: elementStyle.WebkitUserSelect + }; + + dom.setStyles({ + position: "absolute", + top: "-99999px", + left: "-99999px", + // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother + WebkitUserSelect: "none" + }).on(element); + + element.focus(); + + dom.setStyles(originalStyles).on(element); + + if (win.scrollTo) { + // Some browser extensions unset this method to prevent annoyances + // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100 + // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1 + win.scrollTo(originalScrollLeft, originalScrollTop); + } + } + }; + + + wysihtml5.views.Composer.prototype.style = function() { + var that = this, + originalActiveElement = doc.querySelector(":focus"), + textareaElement = this.textarea.element, + hasPlaceholder = textareaElement.hasAttribute("placeholder"), + originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"); + this.focusStylesHost = this.focusStylesHost || HOST_TEMPLATE.cloneNode(false); + this.blurStylesHost = this.blurStylesHost || HOST_TEMPLATE.cloneNode(false); + + // Remove placeholder before copying (as the placeholder has an affect on the computed style) + if (hasPlaceholder) { + textareaElement.removeAttribute("placeholder"); + } + + if (textareaElement === originalActiveElement) { + textareaElement.blur(); + } + + // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) --------- + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost); + + // --------- editor styles --------- + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost); + + // --------- apply standard rules --------- + dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument); + + // --------- :focus styles --------- + focusWithoutScrolling(textareaElement); + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost); + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost); + + // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus + // this is needed for when the change_view event is fired where the iframe is hidden and then + // the blur event fires and re-displays it + var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]); + + // --------- restore focus --------- + if (originalActiveElement) { + originalActiveElement.focus(); + } else { + textareaElement.blur(); + } + + // --------- restore placeholder --------- + if (hasPlaceholder) { + textareaElement.setAttribute("placeholder", originalPlaceholder); + } + + // When copying styles, we only get the computed style which is never returned in percent unit + // Therefore we've to recalculate style onresize + if (!wysihtml5.browser.hasCurrentStyleProperty()) { + var winObserver = dom.observe(win, "resize", function() { + // Remove event listener if composer doesn't exist anymore + if (!dom.contains(document.documentElement, that.iframe)) { + winObserver.stop(); + return; + } + var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement), + originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe); + textareaElement.style.display = ""; + that.iframe.style.display = "none"; + dom.copyStyles(RESIZE_STYLE) + .from(textareaElement) + .to(that.iframe) + .andTo(that.focusStylesHost) + .andTo(that.blurStylesHost); + that.iframe.style.display = originalComposerDisplayStyle; + textareaElement.style.display = originalTextareaDisplayStyle; + }); + } + + // --------- Sync focus/blur styles --------- + this.parent.observe("focus:composer", function() { + dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe); + dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element); + }); + + this.parent.observe("blur:composer", function() { + dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe); + dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); + }); + + return this; + }; +})(wysihtml5);/** + * Taking care of events + * - Simulating 'change' event on contentEditable element + * - Handling drag & drop logic + * - Catch paste events + * - Dispatch proprietary newword:composer event + * - Keyboard shortcuts + */ +(function(wysihtml5) { + var dom = wysihtml5.dom, + browser = wysihtml5.browser, + /** + * Map keyCodes to query commands + */ + shortcuts = { + "66": "bold", // B + "73": "italic", // I + "85": "underline" // U + }; + + wysihtml5.views.Composer.prototype.observe = function() { + var that = this, + state = this.getValue(), + iframe = this.sandbox.getIframe(), + element = this.element, + focusBlurElement = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(), + // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same + pasteEvents = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"]; + + // --------- destroy:composer event --------- + dom.observe(iframe, "DOMNodeRemoved", function() { + clearInterval(domNodeRemovedInterval); + that.parent.fire("destroy:composer"); + }); + + // DOMNodeRemoved event is not supported in IE 8 + var domNodeRemovedInterval = setInterval(function() { + if (!dom.contains(document.documentElement, iframe)) { + clearInterval(domNodeRemovedInterval); + that.parent.fire("destroy:composer"); + } + }, 250); + + + // --------- Focus & blur logic --------- + dom.observe(focusBlurElement, "focus", function() { + that.parent.fire("focus").fire("focus:composer"); + + // Delay storing of state until all focus handler are fired + // especially the one which resets the placeholder + setTimeout(function() { state = that.getValue(); }, 0); + }); + + dom.observe(focusBlurElement, "blur", function() { + if (state !== that.getValue()) { + that.parent.fire("change").fire("change:composer"); + } + that.parent.fire("blur").fire("blur:composer"); + }); + + if (wysihtml5.browser.isIos()) { + // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus + // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible) + // We prevent that by focusing a temporary input element which immediately loses focus + dom.observe(element, "blur", function() { + var input = element.ownerDocument.createElement("input"), + originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop, + originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; + try { + that.selection.insertNode(input); + } catch(e) { + element.appendChild(input); + } + input.focus(); + input.parentNode.removeChild(input); + + window.scrollTo(originalScrollLeft, originalScrollTop); + }); + } + + // --------- Drag & Drop logic --------- + dom.observe(element, "dragenter", function() { + that.parent.fire("unset_placeholder"); + }); + + if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) { + dom.observe(element, ["dragover", "dragenter"], function(event) { + event.preventDefault(); + }); + } + + dom.observe(element, pasteEvents, function(event) { + var dataTransfer = event.dataTransfer, + data; + + if (dataTransfer && browser.supportsDataTransfer()) { + data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain"); + } + if (data) { + element.focus(); + that.commands.exec("insertHTML", data); + that.parent.fire("paste").fire("paste:composer"); + event.stopPropagation(); + event.preventDefault(); + } else { + setTimeout(function() { + that.parent.fire("paste").fire("paste:composer"); + }, 0); + } + }); + + // --------- neword event --------- + dom.observe(element, "keyup", function(event) { + var keyCode = event.keyCode; + if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) { + that.parent.fire("newword:composer"); + } + }); + + this.parent.observe("paste:composer", function() { + setTimeout(function() { that.parent.fire("newword:composer"); }, 0); + }); + + // --------- Make sure that images are selected when clicking on them --------- + if (!browser.canSelectImagesInContentEditable()) { + dom.observe(element, "mousedown", function(event) { + var target = event.target; + if (target.nodeName === "IMG") { + that.selection.selectNode(target); + event.preventDefault(); + } + }); + } + + // --------- Shortcut logic --------- + dom.observe(element, "keydown", function(event) { + var keyCode = event.keyCode, + command = shortcuts[keyCode]; + if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { + that.commands.exec(command); + event.preventDefault(); + } + }); + + // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor --------- + dom.observe(element, "keydown", function(event) { + var target = that.selection.getSelectedNode(true), + keyCode = event.keyCode, + parent; + if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete + parent = target.parentNode; + // delete the <img> + parent.removeChild(target); + // and it's parent <a> too if it hasn't got any other child nodes + if (parent.nodeName === "A" && !parent.firstChild) { + parent.parentNode.removeChild(parent); + } + + setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0); + event.preventDefault(); + } + }); + + // --------- Show url in tooltip when hovering links or images --------- + var titlePrefixes = { + IMG: "Image: ", + A: "Link: " + }; + + dom.observe(element, "mouseover", function(event) { + var target = event.target, + nodeName = target.nodeName, + title; + if (nodeName !== "A" && nodeName !== "IMG") { + return; + } + var hasTitle = target.hasAttribute("title"); + if(!hasTitle){ + title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src")); + target.setAttribute("title", title); + } + }); + }; +})(wysihtml5);/** + * Class that takes care that the value of the composer and the textarea is always in sync + */ +(function(wysihtml5) { + var INTERVAL = 400; + + wysihtml5.views.Synchronizer = Base.extend( + /** @scope wysihtml5.views.Synchronizer.prototype */ { + + constructor: function(editor, textarea, composer) { + this.editor = editor; + this.textarea = textarea; + this.composer = composer; + + this._observe(); + }, + + /** + * Sync html from composer to textarea + * Takes care of placeholders + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea + */ + fromComposerToTextarea: function(shouldParseHtml) { + this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml); + }, + + /** + * Sync value of textarea to composer + * Takes care of placeholders + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer + */ + fromTextareaToComposer: function(shouldParseHtml) { + var textareaValue = this.textarea.getValue(); + if (textareaValue) { + this.composer.setValue(textareaValue, shouldParseHtml); + } else { + this.composer.clear(); + this.editor.fire("set_placeholder"); + } + }, + + /** + * Invoke syncing based on view state + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea + */ + sync: function(shouldParseHtml) { + if (this.editor.currentView.name === "textarea") { + this.fromTextareaToComposer(shouldParseHtml); + } else { + this.fromComposerToTextarea(shouldParseHtml); + } + }, + + /** + * Initializes interval-based syncing + * also makes sure that on-submit the composer's content is synced with the textarea + * immediately when the form gets submitted + */ + _observe: function() { + var interval, + that = this, + form = this.textarea.element.form, + startInterval = function() { + interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL); + }, + stopInterval = function() { + clearInterval(interval); + interval = null; + }; + + startInterval(); + + if (form) { + // If the textarea is in a form make sure that after onreset and onsubmit the composer + // has the correct state + wysihtml5.dom.observe(form, "submit", function() { + that.sync(true); + }); + wysihtml5.dom.observe(form, "reset", function() { + setTimeout(function() { that.fromTextareaToComposer(); }, 0); + }); + } + + this.editor.observe("change_view", function(view) { + if (view === "composer" && !interval) { + that.fromTextareaToComposer(true); + startInterval(); + } else if (view === "textarea") { + that.fromComposerToTextarea(true); + stopInterval(); + } + }); + + this.editor.observe("destroy:composer", stopInterval); + } + }); +})(wysihtml5); +wysihtml5.views.Textarea = wysihtml5.views.View.extend( + /** @scope wysihtml5.views.Textarea.prototype */ { + name: "textarea", + + constructor: function(parent, textareaElement, config) { + this.base(parent, textareaElement, config); + + this._observe(); + }, + + clear: function() { + this.element.value = ""; + }, + + getValue: function(parse) { + var value = this.isEmpty() ? "" : this.element.value; + if (parse) { + value = this.parent.parse(value); + } + return value; + }, + + setValue: function(html, parse) { + if (parse) { + html = this.parent.parse(html); + } + this.element.value = html; + }, + + hasPlaceholderSet: function() { + var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element), + placeholderText = this.element.getAttribute("placeholder") || null, + value = this.element.value, + isEmpty = !value; + return (supportsPlaceholder && isEmpty) || (value === placeholderText); + }, + + isEmpty: function() { + return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet(); + }, + + _observe: function() { + var element = this.element, + parent = this.parent, + eventMapping = { + focusin: "focus", + focusout: "blur" + }, + /** + * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events + * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai + */ + events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"]; + + parent.observe("beforeload", function() { + wysihtml5.dom.observe(element, events, function(event) { + var eventName = eventMapping[event.type] || event.type; + parent.fire(eventName).fire(eventName + ":textarea"); + }); + + wysihtml5.dom.observe(element, ["paste", "drop"], function() { + setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0); + }); + }); + } +});/** + * Toolbar Dialog + * + * @param {Element} link The toolbar link which causes the dialog to show up + * @param {Element} container The dialog container + * + * @example + * <!-- Toolbar link --> + * <a data-wysihtml5-command="insertImage">insert an image</a> + * + * <!-- Dialog --> + * <div data-wysihtml5-dialog="insertImage" style="display: none;"> + * <label> + * URL: <input data-wysihtml5-dialog-field="src" value="http://"> + * </label> + * <label> + * Alternative text: <input data-wysihtml5-dialog-field="alt" value=""> + * </label> + * </div> + * + * <script> + * var dialog = new wysihtml5.toolbar.Dialog( + * document.querySelector("[data-wysihtml5-command='insertImage']"), + * document.querySelector("[data-wysihtml5-dialog='insertImage']") + * ); + * dialog.observe("save", function(attributes) { + * // do something + * }); + * </script> + */ +(function(wysihtml5) { + var dom = wysihtml5.dom, + CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened", + SELECTOR_FORM_ELEMENTS = "input, select, textarea", + SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]", + ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field"; + + + wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend( + /** @scope wysihtml5.toolbar.Dialog.prototype */ { + constructor: function(link, container) { + this.link = link; + this.container = container; + }, + + _observe: function() { + if (this._observed) { + return; + } + + var that = this, + callbackWrapper = function(event) { + var attributes = that._serialize(); + if (attributes == that.elementToChange) { + that.fire("edit", attributes); + } else { + that.fire("save", attributes); + } + that.hide(); + event.preventDefault(); + event.stopPropagation(); + }; + + dom.observe(that.link, "click", function(event) { + if (dom.hasClass(that.link, CLASS_NAME_OPENED)) { + setTimeout(function() { that.hide(); }, 0); + } + }); + + dom.observe(this.container, "keydown", function(event) { + var keyCode = event.keyCode; + if (keyCode === wysihtml5.ENTER_KEY) { + callbackWrapper(event); + } + if (keyCode === wysihtml5.ESCAPE_KEY) { + that.hide(); + } + }); + + dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper); + + dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) { + that.fire("cancel"); + that.hide(); + event.preventDefault(); + event.stopPropagation(); + }); + + var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS), + i = 0, + length = formElements.length, + _clearInterval = function() { clearInterval(that.interval); }; + for (; i<length; i++) { + dom.observe(formElements[i], "change", _clearInterval); + } + + this._observed = true; + }, + + /** + * Grabs all fields in the dialog and puts them in key=>value style in an object which + * then gets returned + */ + _serialize: function() { + var data = this.elementToChange || {}, + fields = this.container.querySelectorAll(SELECTOR_FIELDS), + length = fields.length, + i = 0; + for (; i<length; i++) { + data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value; + } + return data; + }, + + /** + * Takes the attributes of the "elementToChange" + * and inserts them in their corresponding dialog input fields + * + * Assume the "elementToChange" looks like this: + * <a href="http://www.google.com" target="_blank">foo</a> + * + * and we have the following dialog: + * <input type="text" data-wysihtml5-dialog-field="href" value=""> + * <input type="text" data-wysihtml5-dialog-field="target" value=""> + * + * after calling _interpolate() the dialog will look like this + * <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com"> + * <input type="text" data-wysihtml5-dialog-field="target" value="_blank"> + * + * Basically it adopted the attribute values into the corresponding input fields + * + */ + _interpolate: function(avoidHiddenFields) { + var field, + fieldName, + newValue, + focusedElement = document.querySelector(":focus"), + fields = this.container.querySelectorAll(SELECTOR_FIELDS), + length = fields.length, + i = 0; + for (; i<length; i++) { + field = fields[i]; + + // Never change elements where the user is currently typing in + if (field === focusedElement) { + continue; + } + + // Don't update hidden fields + // See https://github.com/xing/wysihtml5/pull/14 + if (avoidHiddenFields && field.type === "hidden") { + continue; + } + + fieldName = field.getAttribute(ATTRIBUTE_FIELDS); + newValue = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue; + field.value = newValue; + } + }, + + /** + * Show the dialog element + */ + show: function(elementToChange) { + var that = this, + firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS); + this.elementToChange = elementToChange; + this._observe(); + this._interpolate(); + if (elementToChange) { + this.interval = setInterval(function() { that._interpolate(true); }, 500); + } + dom.addClass(this.link, CLASS_NAME_OPENED); + this.container.style.display = ""; + this.fire("show"); + if (firstField && !elementToChange) { + try { + firstField.focus(); + } catch(e) {} + } + }, + + /** + * Hide the dialog element + */ + hide: function() { + clearInterval(this.interval); + this.elementToChange = null; + dom.removeClass(this.link, CLASS_NAME_OPENED); + this.container.style.display = "none"; + this.fire("hide"); + } + }); +})(wysihtml5); +/** + * Converts speech-to-text and inserts this into the editor + * As of now (2011/03/25) this only is supported in Chrome >= 11 + * + * Note that it sends the recorded audio to the google speech recognition api: + * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec + * + * Current HTML5 draft can be found here + * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html + * + * "Accessing Google Speech API Chrome 11" + * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ + */ +(function(wysihtml5) { + var dom = wysihtml5.dom; + + var linkStyles = { + position: "relative" + }; + + var wrapperStyles = { + left: 0, + margin: 0, + opacity: 0, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 0, + zIndex: 1 + }; + + var inputStyles = { + cursor: "inherit", + fontSize: "50px", + height: "50px", + marginTop: "-25px", + outline: 0, + padding: 0, + position: "absolute", + right: "-4px", + top: "50%" + }; + + var inputAttributes = { + "x-webkit-speech": "", + "speech": "" + }; + + wysihtml5.toolbar.Speech = function(parent, link) { + var input = document.createElement("input"); + if (!wysihtml5.browser.supportsSpeechApiOn(input)) { + link.style.display = "none"; + return; + } + + var wrapper = document.createElement("div"); + + wysihtml5.lang.object(wrapperStyles).merge({ + width: link.offsetWidth + "px", + height: link.offsetHeight + "px" + }); + + dom.insert(input).into(wrapper); + dom.insert(wrapper).into(link); + + dom.setStyles(inputStyles).on(input); + dom.setAttributes(inputAttributes).on(input) + + dom.setStyles(wrapperStyles).on(wrapper); + dom.setStyles(linkStyles).on(link); + + var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange"; + dom.observe(input, eventName, function() { + parent.execCommand("insertText", input.value); + input.value = ""; + }); + + dom.observe(input, "click", function(event) { + if (dom.hasClass(link, "wysihtml5-command-disabled")) { + event.preventDefault(); + } + + event.stopPropagation(); + }); + }; +})(wysihtml5);/** + * Toolbar + * + * @param {Object} parent Reference to instance of Editor instance + * @param {Element} container Reference to the toolbar container element + * + * @example + * <div id="toolbar"> + * <a data-wysihtml5-command="createLink">insert link</a> + * <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a> + * </div> + * + * <script> + * var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar")); + * </script> + */ +(function(wysihtml5) { + var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled", + CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled", + CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active", + CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active", + dom = wysihtml5.dom; + + wysihtml5.toolbar.Toolbar = Base.extend( + /** @scope wysihtml5.toolbar.Toolbar.prototype */ { + constructor: function(editor, container) { + this.editor = editor; + this.container = typeof(container) === "string" ? document.getElementById(container) : container; + this.composer = editor.composer; + + this._getLinks("command"); + this._getLinks("action"); + + this._observe(); + this.show(); + + var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"), + length = speechInputLinks.length, + i = 0; + for (; i<length; i++) { + new wysihtml5.toolbar.Speech(this, speechInputLinks[i]); + } + }, + + _getLinks: function(type) { + var links = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(), + length = links.length, + i = 0, + mapping = this[type + "Mapping"] = {}, + link, + group, + name, + value, + dialog; + for (; i<length; i++) { + link = links[i]; + name = link.getAttribute("data-wysihtml5-" + type); + value = link.getAttribute("data-wysihtml5-" + type + "-value"); + group = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']"); + dialog = this._getDialog(link, name); + + mapping[name + ":" + value] = { + link: link, + group: group, + name: name, + value: value, + dialog: dialog, + state: false + }; + } + }, + + _getDialog: function(link, command) { + var that = this, + dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"), + dialog, + caretBookmark; + + if (dialogElement) { + dialog = new wysihtml5.toolbar.Dialog(link, dialogElement); + + dialog.observe("show", function() { + caretBookmark = that.composer.selection.getBookmark(); + + that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); + }); + + dialog.observe("save", function(attributes) { + if (caretBookmark) { + that.composer.selection.setBookmark(caretBookmark); + } + that._execCommand(command, attributes); + + that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); + }); + + dialog.observe("cancel", function() { + that.editor.focus(false); + that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); + }); + } + return dialog; + }, + + /** + * @example + * var toolbar = new wysihtml5.Toolbar(); + * // Insert a <blockquote> element or wrap current selection in <blockquote> + * toolbar.execCommand("formatBlock", "blockquote"); + */ + execCommand: function(command, commandValue) { + if (this.commandsDisabled) { + return; + } + + var commandObj = this.commandMapping[command + ":" + commandValue]; + + // Show dialog when available + if (commandObj && commandObj.dialog && !commandObj.state) { + commandObj.dialog.show(); + } else { + this._execCommand(command, commandValue); + } + }, + + _execCommand: function(command, commandValue) { + // Make sure that composer is focussed (false => don't move caret to the end) + this.editor.focus(false); + + this.composer.commands.exec(command, commandValue); + this._updateLinkStates(); + }, + + execAction: function(action) { + var editor = this.editor; + switch(action) { + case "change_view": + if (editor.currentView === editor.textarea) { + editor.fire("change_view", "composer"); + } else { + editor.fire("change_view", "textarea"); + } + break; + } + }, + + _observe: function() { + var that = this, + editor = this.editor, + container = this.container, + links = this.commandLinks.concat(this.actionLinks), + length = links.length, + i = 0; + + for (; i<length; i++) { + // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied + // (you know, a:link { ... } doesn't match anchors with missing href attribute) + dom.setAttributes({ + href: "javascript:;", + unselectable: "on" + }).on(links[i]); + } + + // Needed for opera + dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); }); + + dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) { + var link = this, + command = link.getAttribute("data-wysihtml5-command"), + commandValue = link.getAttribute("data-wysihtml5-command-value"); + that.execCommand(command, commandValue); + event.preventDefault(); + }); + + dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) { + var action = this.getAttribute("data-wysihtml5-action"); + that.execAction(action); + event.preventDefault(); + }); + + editor.observe("focus:composer", function() { + that.bookmark = null; + clearInterval(that.interval); + that.interval = setInterval(function() { that._updateLinkStates(); }, 500); + }); + + editor.observe("blur:composer", function() { + clearInterval(that.interval); + }); + + editor.observe("destroy:composer", function() { + clearInterval(that.interval); + }); + + editor.observe("change_view", function(currentView) { + // Set timeout needed in order to let the blur event fire first + setTimeout(function() { + that.commandsDisabled = (currentView !== "composer"); + that._updateLinkStates(); + if (that.commandsDisabled) { + dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED); + } else { + dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED); + } + }, 0); + }); + }, + + _updateLinkStates: function() { + var element = this.composer.element, + commandMapping = this.commandMapping, + actionMapping = this.actionMapping, + i, + state, + action, + command; + // every millisecond counts... this is executed quite often + for (i in commandMapping) { + command = commandMapping[i]; + if (this.commandsDisabled) { + state = false; + dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE); + if (command.group) { + dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE); + } + if (command.dialog) { + command.dialog.hide(); + } + } else { + state = this.composer.commands.state(command.name, command.value); + if (wysihtml5.lang.object(state).isArray()) { + // Grab first and only object/element in state array, otherwise convert state into boolean + // to avoid showing a dialog for multiple selected elements which may have different attributes + // eg. when two links with different href are selected, the state will be an array consisting of both link elements + // but the dialog interface can only update one + state = state.length === 1 ? state[0] : true; + } + dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED); + if (command.group) { + dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED); + } + } + + if (command.state === state) { + continue; + } + + command.state = state; + if (state) { + dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE); + if (command.group) { + dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE); + } + if (command.dialog) { + if (typeof(state) === "object") { + command.dialog.show(state); + } else { + command.dialog.hide(); + } + } + } else { + dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE); + if (command.group) { + dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE); + } + if (command.dialog) { + command.dialog.hide(); + } + } + } + + for (i in actionMapping) { + action = actionMapping[i]; + + if (action.name === "change_view") { + action.state = this.editor.currentView === this.editor.textarea; + if (action.state) { + dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE); + } else { + dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE); + } + } + } + }, + + show: function() { + this.container.style.display = ""; + }, + + hide: function() { + this.container.style.display = "none"; + } + }); + +})(wysihtml5); +/** + * WYSIHTML5 Editor + * + * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface + * @param {Object} [config] See defaultConfig object below for explanation of each individual config option + * + * @events + * load + * beforeload (for internal use only) + * focus + * focus:composer + * focus:textarea + * blur + * blur:composer + * blur:textarea + * change + * change:composer + * change:textarea + * paste + * paste:composer + * paste:textarea + * newword:composer + * destroy:composer + * undo:composer + * redo:composer + * beforecommand:composer + * aftercommand:composer + * change_view + */ +(function(wysihtml5) { + var undef; + + var defaultConfig = { + // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body + name: undef, + // Whether the editor should look like the textarea (by adopting styles) + style: true, + // Id of the toolbar element, pass falsey value if you don't want any toolbar logic + toolbar: undef, + // Whether urls, entered by the user should automatically become clickable-links + autoLink: true, + // Object which includes parser rules to apply when html gets inserted via copy & paste + // See parser_rules/*.js for examples + parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} }, + // Parser method to use when the user inserts content via copy & paste + parser: wysihtml5.dom.parse, + // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option + composerClassName: "wysihtml5-editor", + // Class name to add to the body when the wysihtml5 editor is supported + bodyClassName: "wysihtml5-supported", + // Array (or single string) of stylesheet urls to be loaded in the editor's iframe + stylesheets: [], + // Placeholder text to use, defaults to the placeholder attribute on the textarea element + placeholderText: undef, + // Whether the composer should allow the user to manually resize images, tables etc. + allowObjectResizing: true, + // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5) + supportTouchDevices: true + }; + + wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend( + /** @scope wysihtml5.Editor.prototype */ { + constructor: function(textareaElement, config) { + this.textareaElement = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement; + this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get(); + this.textarea = new wysihtml5.views.Textarea(this, this.textareaElement, this.config); + this.currentView = this.textarea; + this._isCompatible = wysihtml5.browser.supported(); + + // Sort out unsupported/unwanted browsers here + if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) { + var that = this; + setTimeout(function() { that.fire("beforeload").fire("load"); }, 0); + return; + } + + // Add class name to body, to indicate that the editor is supported + wysihtml5.dom.addClass(document.body, this.config.bodyClassName); + + this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config); + this.currentView = this.composer; + + if (typeof(this.config.parser) === "function") { + this._initParser(); + } + + this.observe("beforeload", function() { + this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer); + if (this.config.toolbar) { + this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar); + } + }); + + try { + console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5"); + } catch(e) {} + }, + + isCompatible: function() { + return this._isCompatible; + }, + + clear: function() { + this.currentView.clear(); + return this; + }, + + getValue: function(parse) { + return this.currentView.getValue(parse); + }, + + setValue: function(html, parse) { + if (!html) { + return this.clear(); + } + this.currentView.setValue(html, parse); + return this; + }, + + focus: function(setToEnd) { + this.currentView.focus(setToEnd); + return this; + }, + + /** + * Deactivate editor (make it readonly) + */ + disable: function() { + this.currentView.disable(); + return this; + }, + + /** + * Activate editor + */ + enable: function() { + this.currentView.enable(); + return this; + }, + + isEmpty: function() { + return this.currentView.isEmpty(); + }, + + hasPlaceholderSet: function() { + return this.currentView.hasPlaceholderSet(); + }, + + parse: function(htmlOrElement) { + var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true); + if (typeof(htmlOrElement) === "object") { + wysihtml5.quirks.redraw(htmlOrElement); + } + return returnValue; + }, + + /** + * Prepare html parser logic + * - Observes for paste and drop + */ + _initParser: function() { + this.observe("paste:composer", function() { + var keepScrollPosition = true, + that = this; + that.composer.selection.executeAndRestore(function() { + wysihtml5.quirks.cleanPastedHTML(that.composer.element); + that.parse(that.composer.element); + }, keepScrollPosition); + }); + + this.observe("paste:textarea", function() { + var value = this.textarea.getValue(), + newValue; + newValue = this.parse(value); + this.textarea.setValue(newValue); + }); + } + }); +})(wysihtml5); diff --git a/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.min.js b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.min.js new file mode 100644 index 0000000..663816a --- /dev/null +++ b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.min.js @@ -0,0 +1,261 @@ +/* + wysihtml5 v0.3.0 + https://github.com/xing/wysihtml5 + + Author: Christopher Blum (https://github.com/tiff) + + Copyright (C) 2012 XING AG + Licensed under the MIT license (MIT) + + Rangy, a cross-browser JavaScript range and selection library + http://code.google.com/p/rangy/ + + Copyright 2011, Tim Down + Licensed under the MIT license. + Version: 1.2.2 + Build date: 13 November 2011 +*/ +var wysihtml5={version:"0.3.0",commands:{},dom:{},quirks:{},toolbar:{},lang:{},selection:{},views:{},INVISIBLE_SPACE:"\ufeff",EMPTY_FUNCTION:function(){},ELEMENT_NODE:1,TEXT_NODE:3,BACKSPACE_KEY:8,ENTER_KEY:13,ESCAPE_KEY:27,SPACE_KEY:32,DELETE_KEY:46}; +window.rangy=function(){function b(a,b){var c=typeof a[b];return c==k||!!(c==h&&a[b])||"unknown"==c}function c(a,b){return!!(typeof a[b]==h&&a[b])}function a(a,b){return typeof a[b]!=j}function d(a){return function(b,c){for(var d=c.length;d--;)if(!a(b,c[d]))return!1;return!0}}function e(a){return a&&m(a,r)&&x(a,q)}function f(a){window.alert("Rangy not supported in your browser. Reason: "+a);o.initialized=!0;o.supported=!1}function g(){if(!o.initialized){var a,d=!1,h=!1;b(document,"createRange")&& +(a=document.createRange(),m(a,p)&&x(a,n)&&(d=!0),a.detach());if((a=c(document,"body")?document.body:document.getElementsByTagName("body")[0])&&b(a,"createTextRange"))a=a.createTextRange(),e(a)&&(h=!0);!d&&!h&&f("Neither Range nor TextRange are implemented");o.initialized=!0;o.features={implementsDomRange:d,implementsTextRange:h};d=w.concat(z);h=0;for(a=d.length;h<a;++h)try{d[h](o)}catch(j){c(window,"console")&&b(window.console,"log")&&window.console.log("Init listener threw an exception. Continuing.", +j)}}}function i(a){this.name=a;this.supported=this.initialized=!1}var h="object",k="function",j="undefined",n="startContainer startOffset endContainer endOffset collapsed commonAncestorContainer START_TO_START START_TO_END END_TO_START END_TO_END".split(" "),p="setStart setStartBefore setStartAfter setEnd setEndBefore setEndAfter collapse selectNode selectNodeContents compareBoundaryPoints deleteContents extractContents cloneContents insertNode surroundContents cloneRange toString detach".split(" "), +q="boundingHeight boundingLeft boundingTop boundingWidth htmlText text".split(" "),r="collapse compareEndPoints duplicate getBookmark moveToBookmark moveToElementText parentElement pasteHTML select setEndPoint getBoundingClientRect".split(" "),m=d(b),s=d(c),x=d(a),o={version:"1.2.2",initialized:!1,supported:!0,util:{isHostMethod:b,isHostObject:c,isHostProperty:a,areHostMethods:m,areHostObjects:s,areHostProperties:x,isTextRange:e},features:{},modules:{},config:{alertOnWarn:!1,preferTextRange:!1}}; +o.fail=f;o.warn=function(a){a="Rangy warning: "+a;o.config.alertOnWarn?window.alert(a):typeof window.console!=j&&typeof window.console.log!=j&&window.console.log(a)};({}).hasOwnProperty?o.util.extend=function(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c])}:f("hasOwnProperty not supported");var z=[],w=[];o.init=g;o.addInitListener=function(a){o.initialized?a(o):z.push(a)};var y=[];o.addCreateMissingNativeApiListener=function(a){y.push(a)};o.createMissingNativeApi=function(a){a=a||window;g(); +for(var b=0,c=y.length;b<c;++b)y[b](a)};i.prototype.fail=function(a){this.initialized=!0;this.supported=!1;throw Error("Module '"+this.name+"' failed to load: "+a);};i.prototype.warn=function(a){o.warn("Module "+this.name+": "+a)};i.prototype.createError=function(a){return Error("Error in Rangy "+this.name+" module: "+a)};o.createModule=function(a,b){var c=new i(a);o.modules[a]=c;w.push(function(a){b(a,c);c.initialized=!0;c.supported=!0})};o.requireModules=function(a){for(var b=0,c=a.length,d,h;b< +c;++b){h=a[b];d=o.modules[h];if(!d||!(d instanceof i))throw Error("Module '"+h+"' not found");if(!d.supported)throw Error("Module '"+h+"' not supported");}};var A=!1,s=function(){A||(A=!0,o.initialized||g())};if(typeof window==j)f("No window found");else if(typeof document==j)f("No document found");else return b(document,"addEventListener")&&document.addEventListener("DOMContentLoaded",s,!1),b(window,"addEventListener")?window.addEventListener("load",s,!1):b(window,"attachEvent")?window.attachEvent("onload", +s):f("Window does not have required addEventListener or attachEvent method"),o}(); +rangy.createModule("DomUtil",function(b,c){function a(a){for(var b=0;a=a.previousSibling;)b++;return b}function d(a,b){var c=[],d;for(d=a;d;d=d.parentNode)c.push(d);for(d=b;d;d=d.parentNode)if(m(c,d))return d;return null}function e(a,b,c){for(c=c?a:a.parentNode;c;){a=c.parentNode;if(a===b)return c;c=a}return null}function f(a){a=a.nodeType;return 3==a||4==a||8==a}function g(a,b){var c=b.nextSibling,d=b.parentNode;c?d.insertBefore(a,c):d.appendChild(a);return a}function i(a){if(9==a.nodeType)return a; +if(typeof a.ownerDocument!=p)return a.ownerDocument;if(typeof a.document!=p)return a.document;if(a.parentNode)return i(a.parentNode);throw Error("getDocument: no document found for node");}function h(a){return!a?"[No node]":f(a)?'"'+a.data+'"':1==a.nodeType?"<"+a.nodeName+(a.id?' id="'+a.id+'"':"")+">["+a.childNodes.length+"]":a.nodeName}function k(a){this._next=this.root=a}function j(a,b){this.node=a;this.offset=b}function n(a){this.code=this[a];this.codeName=a;this.message="DOMException: "+this.codeName} +var p="undefined",q=b.util;q.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||c.fail("document missing a Node creation method");q.isHostMethod(document,"getElementsByTagName")||c.fail("document missing getElementsByTagName method");var r=document.createElement("div");q.areHostMethods(r,["insertBefore","appendChild","cloneNode"])||c.fail("Incomplete Element implementation");q.isHostProperty(r,"innerHTML")||c.fail("Element is missing innerHTML property");r=document.createTextNode("test"); +q.areHostMethods(r,["splitText","deleteData","insertData","appendData","cloneNode"])||c.fail("Incomplete Text Node implementation");var m=function(a,b){for(var c=a.length;c--;)if(a[c]===b)return!0;return!1};k.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var a=this._current=this._next,b;if(this._current){b=a.firstChild;if(!b)for(b=null;a!==this.root&&!(b=a.nextSibling);)a=a.parentNode;this._next=b}return this._current},detach:function(){this._current=this._next=this.root= +null}};j.prototype={equals:function(a){return this.node===a.node&this.offset==a.offset},inspect:function(){return"[DomPosition("+h(this.node)+":"+this.offset+")]"}};n.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11};n.prototype.toString=function(){return this.message};b.dom={arrayContains:m,isHtmlNamespace:function(a){var b;return typeof a.namespaceURI==p||null===(b=a.namespaceURI)||"http://www.w3.org/1999/xhtml"== +b},parentElement:function(a){a=a.parentNode;return 1==a.nodeType?a:null},getNodeIndex:a,getNodeLength:function(a){var b;return f(a)?a.length:(b=a.childNodes)?b.length:0},getCommonAncestor:d,isAncestorOf:function(a,b,c){for(b=c?b:b.parentNode;b;){if(b===a)return!0;b=b.parentNode}return!1},getClosestAncestorIn:e,isCharacterDataNode:f,insertAfter:g,splitDataNode:function(a,b){var c=a.cloneNode(!1);c.deleteData(0,b);a.deleteData(b,a.length-b);g(c,a);return c},getDocument:i,getWindow:function(a){a=i(a); +if(typeof a.defaultView!=p)return a.defaultView;if(typeof a.parentWindow!=p)return a.parentWindow;throw Error("Cannot get a window object for node");},getIframeWindow:function(a){if(typeof a.contentWindow!=p)return a.contentWindow;if(typeof a.contentDocument!=p)return a.contentDocument.defaultView;throw Error("getIframeWindow: No Window object found for iframe element");},getIframeDocument:function(a){if(typeof a.contentDocument!=p)return a.contentDocument;if(typeof a.contentWindow!=p)return a.contentWindow.document; +throw Error("getIframeWindow: No Document object found for iframe element");},getBody:function(a){return q.isHostObject(a,"body")?a.body:a.getElementsByTagName("body")[0]},getRootContainer:function(a){for(var b;b=a.parentNode;)a=b;return a},comparePoints:function(b,c,h,j){var k;if(b==h)return c===j?0:c<j?-1:1;if(k=e(h,b,!0))return c<=a(k)?-1:1;if(k=e(b,h,!0))return a(k)<j?-1:1;c=d(b,h);b=b===c?c:e(b,c,!0);h=h===c?c:e(h,c,!0);if(b===h)throw Error("comparePoints got to case 4 and childA and childB are the same!"); +for(c=c.firstChild;c;){if(c===b)return-1;if(c===h)return 1;c=c.nextSibling}throw Error("Should not be here!");},inspectNode:h,fragmentFromNodeChildren:function(a){for(var b=i(a).createDocumentFragment(),c;c=a.firstChild;)b.appendChild(c);return b},createIterator:function(a){return new k(a)},DomPosition:j};b.DOMException=n}); +rangy.createModule("DomRange",function(b){function c(a,b){return 3!=a.nodeType&&(l.isAncestorOf(a,b.startContainer,!0)||l.isAncestorOf(a,b.endContainer,!0))}function a(a){return l.getDocument(a.startContainer)}function d(a,b,c){if(b=a._listeners[b])for(var d=0,h=b.length;d<h;++d)b[d].call(a,{target:a,args:c})}function e(a){return new u(a.parentNode,l.getNodeIndex(a))}function f(a){return new u(a.parentNode,l.getNodeIndex(a)+1)}function g(a,b,c){var d=11==a.nodeType?a.firstChild:a;l.isCharacterDataNode(b)? +c==b.length?l.insertAfter(a,b):b.parentNode.insertBefore(a,0==c?b:l.splitDataNode(b,c)):c>=b.childNodes.length?b.appendChild(a):b.insertBefore(a,b.childNodes[c]);return d}function i(b){for(var c,d,h=a(b.range).createDocumentFragment();d=b.next();){c=b.isPartiallySelectedSubtree();d=d.cloneNode(!c);c&&(c=b.getSubtreeIterator(),d.appendChild(i(c)),c.detach(!0));if(10==d.nodeType)throw new B("HIERARCHY_REQUEST_ERR");h.appendChild(d)}return h}function h(a,b,c){for(var d,e,c=c||{stop:!1};d=a.next();)if(a.isPartiallySelectedSubtree())if(!1=== +b(d)){c.stop=!0;break}else{if(d=a.getSubtreeIterator(),h(d,b,c),d.detach(!0),c.stop)break}else for(d=l.createIterator(d);e=d.next();)if(!1===b(e)){c.stop=!0;return}}function k(a){for(var b;a.next();)a.isPartiallySelectedSubtree()?(b=a.getSubtreeIterator(),k(b),b.detach(!0)):a.remove()}function j(b){for(var c,d=a(b.range).createDocumentFragment(),h;c=b.next();){b.isPartiallySelectedSubtree()?(c=c.cloneNode(!1),h=b.getSubtreeIterator(),c.appendChild(j(h)),h.detach(!0)):b.remove();if(10==c.nodeType)throw new B("HIERARCHY_REQUEST_ERR"); +d.appendChild(c)}return d}function n(a,b,c){var d=!(!b||!b.length),e,j=!!c;d&&(e=RegExp("^("+b.join("|")+")$"));var k=[];h(new q(a,!1),function(a){(!d||e.test(a.nodeType))&&(!j||c(a))&&k.push(a)});return k}function p(a){return"["+("undefined"==typeof a.getName?"Range":a.getName())+"("+l.inspectNode(a.startContainer)+":"+a.startOffset+", "+l.inspectNode(a.endContainer)+":"+a.endOffset+")]"}function q(a,b){this.range=a;this.clonePartiallySelectedTextNodes=b;if(!a.collapsed){this.sc=a.startContainer; +this.so=a.startOffset;this.ec=a.endContainer;this.eo=a.endOffset;var c=a.commonAncestorContainer;this.sc===this.ec&&l.isCharacterDataNode(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc===c&&!l.isCharacterDataNode(this.sc)?this.sc.childNodes[this.so]:l.getClosestAncestorIn(this.sc,c,!0),this._last=this.ec===c&&!l.isCharacterDataNode(this.ec)?this.ec.childNodes[this.eo-1]:l.getClosestAncestorIn(this.ec,c,!0))}}function r(a){this.code= +this[a];this.codeName=a;this.message="RangeException: "+this.codeName}function m(a,b,c){this.nodes=n(a,b,c);this._next=this.nodes[0];this._position=0}function s(a){return function(b,c){for(var d,h=c?b:b.parentNode;h;){d=h.nodeType;if(l.arrayContains(a,d))return h;h=h.parentNode}return null}}function x(a,b){if($(a,b))throw new r("INVALID_NODE_TYPE_ERR");}function o(a){if(!a.startContainer)throw new B("INVALID_STATE_ERR");}function z(a,b){if(!l.arrayContains(b,a.nodeType))throw new r("INVALID_NODE_TYPE_ERR"); +}function w(a,b){if(0>b||b>(l.isCharacterDataNode(a)?a.length:a.childNodes.length))throw new B("INDEX_SIZE_ERR");}function y(a,b){if(O(a,!0)!==O(b,!0))throw new B("WRONG_DOCUMENT_ERR");}function A(a){if(aa(a,!0))throw new B("NO_MODIFICATION_ALLOWED_ERR");}function t(a,b){if(!a)throw new B(b);}function v(a){o(a);if(!l.arrayContains(G,a.startContainer.nodeType)&&!O(a.startContainer,!0)||!l.arrayContains(G,a.endContainer.nodeType)&&!O(a.endContainer,!0)||!(a.startOffset<=(l.isCharacterDataNode(a.startContainer)? +a.startContainer.length:a.startContainer.childNodes.length))||!(a.endOffset<=(l.isCharacterDataNode(a.endContainer)?a.endContainer.length:a.endContainer.childNodes.length)))throw Error("Range error: Range is no longer valid after DOM mutation ("+a.inspect()+")");}function D(){}function K(a){a.START_TO_START=Q;a.START_TO_END=U;a.END_TO_END=ba;a.END_TO_START=V;a.NODE_BEFORE=W;a.NODE_AFTER=X;a.NODE_BEFORE_AND_AFTER=Y;a.NODE_INSIDE=R}function F(a){K(a);K(a.prototype)}function E(a,b){return function(){v(this); +var c=this.startContainer,d=this.startOffset,e=this.commonAncestorContainer,j=new q(this,!0);c!==e&&(c=l.getClosestAncestorIn(c,e,!0),d=f(c),c=d.node,d=d.offset);h(j,A);j.reset();e=a(j);j.detach();b(this,c,d,c,d);return e}}function I(a,d,h){function g(a,b){return function(c){o(this);z(c,L);z(M(c),G);c=(a?e:f)(c);(b?i:n)(this,c.node,c.offset)}}function i(a,b,c){var h=a.endContainer,e=a.endOffset;if(b!==a.startContainer||c!==a.startOffset){if(M(b)!=M(h)||1==l.comparePoints(b,c,h,e))h=b,e=c;d(a,b,c, +h,e)}}function n(a,b,c){var h=a.startContainer,e=a.startOffset;if(b!==a.endContainer||c!==a.endOffset){if(M(b)!=M(h)||-1==l.comparePoints(b,c,h,e))h=b,e=c;d(a,h,e,b,c)}}a.prototype=new D;b.util.extend(a.prototype,{setStart:function(a,b){o(this);x(a,!0);w(a,b);i(this,a,b)},setEnd:function(a,b){o(this);x(a,!0);w(a,b);n(this,a,b)},setStartBefore:g(!0,!0),setStartAfter:g(!1,!0),setEndBefore:g(!0,!1),setEndAfter:g(!1,!1),collapse:function(a){v(this);a?d(this,this.startContainer,this.startOffset,this.startContainer, +this.startOffset):d(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(a){o(this);x(a,!0);d(this,a,0,a,l.getNodeLength(a))},selectNode:function(a){o(this);x(a,!1);z(a,L);var b=e(a),a=f(a);d(this,b.node,b.offset,a.node,a.offset)},extractContents:E(j,d),deleteContents:E(k,d),canSurroundContents:function(){v(this);A(this.startContainer);A(this.endContainer);var a=new q(this,!0),b=a._first&&c(a._first,this)||a._last&&c(a._last,this);a.detach();return!b}, +detach:function(){h(this)},splitBoundaries:function(){v(this);var a=this.startContainer,b=this.startOffset,c=this.endContainer,h=this.endOffset,e=a===c;l.isCharacterDataNode(c)&&(0<h&&h<c.length)&&l.splitDataNode(c,h);l.isCharacterDataNode(a)&&(0<b&&b<a.length)&&(a=l.splitDataNode(a,b),e?(h-=b,c=a):c==a.parentNode&&h>=l.getNodeIndex(a)&&h++,b=0);d(this,a,b,c,h)},normalizeBoundaries:function(){v(this);var a=this.startContainer,b=this.startOffset,c=this.endContainer,h=this.endOffset,e=function(a){var b= +a.nextSibling;b&&b.nodeType==a.nodeType&&(c=a,h=a.length,a.appendData(b.data),b.parentNode.removeChild(b))},j=function(d){var e=d.previousSibling;if(e&&e.nodeType==d.nodeType){a=d;var j=d.length;b=e.length;d.insertData(0,e.data);e.parentNode.removeChild(e);a==c?(h+=b,c=a):c==d.parentNode&&(e=l.getNodeIndex(d),h==e?(c=d,h=j):h>e&&h--)}},k=!0;l.isCharacterDataNode(c)?c.length==h&&e(c):(0<h&&(k=c.childNodes[h-1])&&l.isCharacterDataNode(k)&&e(k),k=!this.collapsed);k?l.isCharacterDataNode(a)?0==b&&j(a): +b<a.childNodes.length&&(e=a.childNodes[b])&&l.isCharacterDataNode(e)&&j(e):(a=c,b=h);d(this,a,b,c,h)},collapseToPoint:function(a,b){o(this);x(a,!0);w(a,b);(a!==this.startContainer||b!==this.startOffset||a!==this.endContainer||b!==this.endOffset)&&d(this,a,b,a,b)}});F(a)}function N(a){a.collapsed=a.startContainer===a.endContainer&&a.startOffset===a.endOffset;a.commonAncestorContainer=a.collapsed?a.startContainer:l.getCommonAncestor(a.startContainer,a.endContainer)}function J(a,b,c,h,e){var j=a.startContainer!== +b||a.startOffset!==c,k=a.endContainer!==h||a.endOffset!==e;a.startContainer=b;a.startOffset=c;a.endContainer=h;a.endOffset=e;N(a);d(a,"boundarychange",{startMoved:j,endMoved:k})}function C(a){this.startContainer=a;this.startOffset=0;this.endContainer=a;this.endOffset=0;this._listeners={boundarychange:[],detach:[]};N(this)}b.requireModules(["DomUtil"]);var l=b.dom,u=l.DomPosition,B=b.DOMException;q.prototype={_current:null,_next:null,_first:null,_last:null,isSingleCharacterDataNode:!1,reset:function(){this._current= +null;this._next=this._first},hasNext:function(){return!!this._next},next:function(){var a=this._current=this._next;a&&(this._next=a!==this._last?a.nextSibling:null,l.isCharacterDataNode(a)&&this.clonePartiallySelectedTextNodes&&(a===this.ec&&(a=a.cloneNode(!0)).deleteData(this.eo,a.length-this.eo),this._current===this.sc&&(a=a.cloneNode(!0)).deleteData(0,this.so)));return a},remove:function(){var a=this._current,b,c;l.isCharacterDataNode(a)&&(a===this.sc||a===this.ec)?(b=a===this.sc?this.so:0,c=a=== +this.ec?this.eo:a.length,b!=c&&a.deleteData(b,c-b)):a.parentNode&&a.parentNode.removeChild(a)},isPartiallySelectedSubtree:function(){return c(this._current,this.range)},getSubtreeIterator:function(){var b;if(this.isSingleCharacterDataNode)b=this.range.cloneRange(),b.collapse();else{b=new C(a(this.range));var c=this._current,d=c,h=0,e=c,j=l.getNodeLength(c);l.isAncestorOf(c,this.sc,!0)&&(d=this.sc,h=this.so);l.isAncestorOf(c,this.ec,!0)&&(e=this.ec,j=this.eo);J(b,d,h,e,j)}return new q(b,this.clonePartiallySelectedTextNodes)}, +detach:function(a){a&&this.range.detach();this.range=this._current=this._next=this._first=this._last=this.sc=this.so=this.ec=this.eo=null}};r.prototype={BAD_BOUNDARYPOINTS_ERR:1,INVALID_NODE_TYPE_ERR:2};r.prototype.toString=function(){return this.message};m.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){this._current=this._next;this._next=this.nodes[++this._position];return this._current},detach:function(){this._current=this._next=this.nodes=null}};var L=[1,3,4,5, +7,8,10],G=[2,9,11],P=[1,3,4,5,7,8,10,11],H=[1,3,4,5,7,8],M=l.getRootContainer,O=s([9,11]),aa=s([5,6,10,12]),$=s([6,10,12]),Z=document.createElement("style"),S=!1;try{Z.innerHTML="<b>x</b>",S=3==Z.firstChild.nodeType}catch(ca){}b.features.htmlParsingConforms=S;var T="startContainer startOffset endContainer endOffset collapsed commonAncestorContainer".split(" "),Q=0,U=1,ba=2,V=3,W=0,X=1,Y=2,R=3;D.prototype={attachListener:function(a,b){this._listeners[a].push(b)},compareBoundaryPoints:function(a,b){v(this); +y(this.startContainer,b.startContainer);var c=a==V||a==Q?"start":"end",d=a==U||a==Q?"start":"end";return l.comparePoints(this[c+"Container"],this[c+"Offset"],b[d+"Container"],b[d+"Offset"])},insertNode:function(a){v(this);z(a,P);A(this.startContainer);if(l.isAncestorOf(a,this.startContainer,!0))throw new B("HIERARCHY_REQUEST_ERR");this.setStartBefore(g(a,this.startContainer,this.startOffset))},cloneContents:function(){v(this);var b,c;if(this.collapsed)return a(this).createDocumentFragment();if(this.startContainer=== +this.endContainer&&l.isCharacterDataNode(this.startContainer))return b=this.startContainer.cloneNode(!0),b.data=b.data.slice(this.startOffset,this.endOffset),c=a(this).createDocumentFragment(),c.appendChild(b),c;c=new q(this,!0);b=i(c);c.detach();return b},canSurroundContents:function(){v(this);A(this.startContainer);A(this.endContainer);var a=new q(this,!0),b=a._first&&c(a._first,this)||a._last&&c(a._last,this);a.detach();return!b},surroundContents:function(a){z(a,H);if(!this.canSurroundContents())throw new r("BAD_BOUNDARYPOINTS_ERR"); +var b=this.extractContents();if(a.hasChildNodes())for(;a.lastChild;)a.removeChild(a.lastChild);g(a,this.startContainer,this.startOffset);a.appendChild(b);this.selectNode(a)},cloneRange:function(){v(this);for(var b=new C(a(this)),c=T.length,d;c--;)d=T[c],b[d]=this[d];return b},toString:function(){v(this);var a=this.startContainer;if(a===this.endContainer&&l.isCharacterDataNode(a))return 3==a.nodeType||4==a.nodeType?a.data.slice(this.startOffset,this.endOffset):"";var b=[],a=new q(this,!0);h(a,function(a){(3== +a.nodeType||4==a.nodeType)&&b.push(a.data)});a.detach();return b.join("")},compareNode:function(a){v(this);var b=a.parentNode,c=l.getNodeIndex(a);if(!b)throw new B("NOT_FOUND_ERR");a=this.comparePoint(b,c);b=this.comparePoint(b,c+1);return 0>a?0<b?Y:W:0<b?X:R},comparePoint:function(a,b){v(this);t(a,"HIERARCHY_REQUEST_ERR");y(a,this.startContainer);return 0>l.comparePoints(a,b,this.startContainer,this.startOffset)?-1:0<l.comparePoints(a,b,this.endContainer,this.endOffset)?1:0},createContextualFragment:S? +function(a){var b=this.startContainer,c=l.getDocument(b);if(!b)throw new B("INVALID_STATE_ERR");var d=null;1==b.nodeType?d=b:l.isCharacterDataNode(b)&&(d=l.parentElement(b));d=null===d||"HTML"==d.nodeName&&l.isHtmlNamespace(l.getDocument(d).documentElement)&&l.isHtmlNamespace(d)?c.createElement("body"):d.cloneNode(!1);d.innerHTML=a;return l.fragmentFromNodeChildren(d)}:function(b){o(this);var c=a(this).createElement("body");c.innerHTML=b;return l.fragmentFromNodeChildren(c)},toHtml:function(){v(this); +var b=a(this).createElement("div");b.appendChild(this.cloneContents());return b.innerHTML},intersectsNode:function(b,c){v(this);t(b,"NOT_FOUND_ERR");if(l.getDocument(b)!==a(this))return!1;var d=b.parentNode,h=l.getNodeIndex(b);t(d,"NOT_FOUND_ERR");var e=l.comparePoints(d,h,this.endContainer,this.endOffset),d=l.comparePoints(d,h+1,this.startContainer,this.startOffset);return c?0>=e&&0<=d:0>e&&0<d},isPointInRange:function(a,b){v(this);t(a,"HIERARCHY_REQUEST_ERR");y(a,this.startContainer);return 0<= +l.comparePoints(a,b,this.startContainer,this.startOffset)&&0>=l.comparePoints(a,b,this.endContainer,this.endOffset)},intersectsRange:function(b,c){v(this);if(a(b)!=a(this))throw new B("WRONG_DOCUMENT_ERR");var d=l.comparePoints(this.startContainer,this.startOffset,b.endContainer,b.endOffset),h=l.comparePoints(this.endContainer,this.endOffset,b.startContainer,b.startOffset);return c?0>=d&&0<=h:0>d&&0<h},intersection:function(a){if(this.intersectsRange(a)){var b=l.comparePoints(this.startContainer, +this.startOffset,a.startContainer,a.startOffset),c=l.comparePoints(this.endContainer,this.endOffset,a.endContainer,a.endOffset),d=this.cloneRange();-1==b&&d.setStart(a.startContainer,a.startOffset);1==c&&d.setEnd(a.endContainer,a.endOffset);return d}return null},union:function(a){if(this.intersectsRange(a,!0)){var b=this.cloneRange();-1==l.comparePoints(a.startContainer,a.startOffset,this.startContainer,this.startOffset)&&b.setStart(a.startContainer,a.startOffset);1==l.comparePoints(a.endContainer, +a.endOffset,this.endContainer,this.endOffset)&&b.setEnd(a.endContainer,a.endOffset);return b}throw new r("Ranges do not intersect");},containsNode:function(a,b){return b?this.intersectsNode(a,!1):this.compareNode(a)==R},containsNodeContents:function(a){return 0<=this.comparePoint(a,0)&&0>=this.comparePoint(a,l.getNodeLength(a))},containsRange:function(a){return this.intersection(a).equals(a)},containsNodeText:function(a){var b=this.cloneRange();b.selectNode(a);var c=b.getNodes([3]);return 0<c.length? +(b.setStart(c[0],0),a=c.pop(),b.setEnd(a,a.length),a=this.containsRange(b),b.detach(),a):this.containsNodeContents(a)},createNodeIterator:function(a,b){v(this);return new m(this,a,b)},getNodes:function(a,b){v(this);return n(this,a,b)},getDocument:function(){return a(this)},collapseBefore:function(a){o(this);this.setEndBefore(a);this.collapse(!1)},collapseAfter:function(a){o(this);this.setStartAfter(a);this.collapse(!0)},getName:function(){return"DomRange"},equals:function(a){return C.rangesEqual(this, +a)},inspect:function(){return p(this)}};I(C,J,function(a){o(a);a.startContainer=a.startOffset=a.endContainer=a.endOffset=null;a.collapsed=a.commonAncestorContainer=null;d(a,"detach",null);a._listeners=null});b.rangePrototype=D.prototype;C.rangeProperties=T;C.RangeIterator=q;C.copyComparisonConstants=F;C.createPrototypeRange=I;C.inspect=p;C.getRangeDocument=a;C.rangesEqual=function(a,b){return a.startContainer===b.startContainer&&a.startOffset===b.startOffset&&a.endContainer===b.endContainer&&a.endOffset=== +b.endOffset};b.DomRange=C;b.RangeException=r}); +rangy.createModule("WrappedRange",function(b){function c(a,b,c,d){var g=a.duplicate();g.collapse(c);var i=g.parentElement();e.isAncestorOf(b,i,!0)||(i=b);if(!i.canHaveHTML)return new f(i.parentNode,e.getNodeIndex(i));var b=e.getDocument(i).createElement("span"),r,m=c?"StartToStart":"StartToEnd";do i.insertBefore(b,b.previousSibling),g.moveToElementText(b);while(0<(r=g.compareEndPoints(m,a))&&b.previousSibling);m=b.nextSibling;if(-1==r&&m&&e.isCharacterDataNode(m)){g.setEndPoint(c?"EndToStart":"EndToEnd", +a);if(/[\r\n]/.test(m.data)){i=g.duplicate();c=i.text.replace(/\r\n/g,"\r").length;for(c=i.moveStart("character",c);-1==i.compareEndPoints("StartToEnd",i);)c++,i.moveStart("character",1)}else c=g.text.length;i=new f(m,c)}else m=(d||!c)&&b.previousSibling,i=(c=(d||c)&&b.nextSibling)&&e.isCharacterDataNode(c)?new f(c,0):m&&e.isCharacterDataNode(m)?new f(m,m.length):new f(i,e.getNodeIndex(b));b.parentNode.removeChild(b);return i}function a(a,b){var c,d,f=a.offset,g=e.getDocument(a.node),i=g.body.createTextRange(), +m=e.isCharacterDataNode(a.node);m?(c=a.node,d=c.parentNode):(c=a.node.childNodes,c=f<c.length?c[f]:null,d=a.node);g=g.createElement("span");g.innerHTML="&#feff;";c?d.insertBefore(g,c):d.appendChild(g);i.moveToElementText(g);i.collapse(!b);d.removeChild(g);if(m)i[b?"moveStart":"moveEnd"]("character",f);return i}b.requireModules(["DomUtil","DomRange"]);var d,e=b.dom,f=e.DomPosition,g=b.DomRange;if(b.features.implementsDomRange&&(!b.features.implementsTextRange||!b.config.preferTextRange))(function(){function a(b){for(var c= +j.length,d;c--;)d=j[c],b[d]=b.nativeRange[d]}var c,j=g.rangeProperties,f;d=function(b){if(!b)throw Error("Range must be specified");this.nativeRange=b;a(this)};g.createPrototypeRange(d,function(a,b,c,d,h){var e=a.endContainer!==d||a.endOffset!=h;if(a.startContainer!==b||a.startOffset!=c||e)a.setEnd(d,h),a.setStart(b,c)},function(a){a.nativeRange.detach();a.detached=!0;for(var b=j.length,c;b--;)c=j[b],a[c]=null});c=d.prototype;c.selectNode=function(b){this.nativeRange.selectNode(b);a(this)};c.deleteContents= +function(){this.nativeRange.deleteContents();a(this)};c.extractContents=function(){var b=this.nativeRange.extractContents();a(this);return b};c.cloneContents=function(){return this.nativeRange.cloneContents()};c.surroundContents=function(b){this.nativeRange.surroundContents(b);a(this)};c.collapse=function(b){this.nativeRange.collapse(b);a(this)};c.cloneRange=function(){return new d(this.nativeRange.cloneRange())};c.refresh=function(){a(this)};c.toString=function(){return this.nativeRange.toString()}; +var i=document.createTextNode("test");e.getBody(document).appendChild(i);var q=document.createRange();q.setStart(i,0);q.setEnd(i,0);try{q.setStart(i,1),c.setStart=function(b,c){this.nativeRange.setStart(b,c);a(this)},c.setEnd=function(b,c){this.nativeRange.setEnd(b,c);a(this)},f=function(b){return function(c){this.nativeRange[b](c);a(this)}}}catch(r){c.setStart=function(b,c){try{this.nativeRange.setStart(b,c)}catch(d){this.nativeRange.setEnd(b,c),this.nativeRange.setStart(b,c)}a(this)},c.setEnd=function(b, +c){try{this.nativeRange.setEnd(b,c)}catch(d){this.nativeRange.setStart(b,c),this.nativeRange.setEnd(b,c)}a(this)},f=function(b,c){return function(d){try{this.nativeRange[b](d)}catch(e){this.nativeRange[c](d),this.nativeRange[b](d)}a(this)}}}c.setStartBefore=f("setStartBefore","setEndBefore");c.setStartAfter=f("setStartAfter","setEndAfter");c.setEndBefore=f("setEndBefore","setStartBefore");c.setEndAfter=f("setEndAfter","setStartAfter");q.selectNodeContents(i);c.selectNodeContents=q.startContainer== +i&&q.endContainer==i&&0==q.startOffset&&q.endOffset==i.length?function(b){this.nativeRange.selectNodeContents(b);a(this)}:function(a){this.setStart(a,0);this.setEnd(a,g.getEndOffset(a))};q.selectNodeContents(i);q.setEnd(i,3);f=document.createRange();f.selectNodeContents(i);f.setEnd(i,4);f.setStart(i,2);c.compareBoundaryPoints=-1==q.compareBoundaryPoints(q.START_TO_END,f)&1==q.compareBoundaryPoints(q.END_TO_START,f)?function(a,b){b=b.nativeRange||b;a==b.START_TO_END?a=b.END_TO_START:a==b.END_TO_START&& +(a=b.START_TO_END);return this.nativeRange.compareBoundaryPoints(a,b)}:function(a,b){return this.nativeRange.compareBoundaryPoints(a,b.nativeRange||b)};b.util.isHostMethod(q,"createContextualFragment")&&(c.createContextualFragment=function(a){return this.nativeRange.createContextualFragment(a)});e.getBody(document).removeChild(i);q.detach();f.detach()})(),b.createNativeRange=function(a){a=a||document;return a.createRange()};else if(b.features.implementsTextRange){d=function(a){this.textRange=a;this.refresh()}; +d.prototype=new g(document);d.prototype.refresh=function(){var a,b,d=this.textRange;a=d.parentElement();var f=d.duplicate();f.collapse(!0);b=f.parentElement();f=d.duplicate();f.collapse(!1);d=f.parentElement();b=b==d?b:e.getCommonAncestor(b,d);b=b==a?b:e.getCommonAncestor(a,b);0==this.textRange.compareEndPoints("StartToEnd",this.textRange)?b=a=c(this.textRange,b,!0,!0):(a=c(this.textRange,b,!0,!1),b=c(this.textRange,b,!1,!1));this.setStart(a.node,a.offset);this.setEnd(b.node,b.offset)};g.copyComparisonConstants(d); +var i=function(){return this}();"undefined"==typeof i.Range&&(i.Range=d);b.createNativeRange=function(a){a=a||document;return a.body.createTextRange()}}b.features.implementsTextRange&&(d.rangeToTextRange=function(b){if(b.collapsed)return a(new f(b.startContainer,b.startOffset),!0);var c=a(new f(b.startContainer,b.startOffset),!0),d=a(new f(b.endContainer,b.endOffset),!1),b=e.getDocument(b.startContainer).body.createTextRange();b.setEndPoint("StartToStart",c);b.setEndPoint("EndToEnd",d);return b}); +d.prototype.getName=function(){return"WrappedRange"};b.WrappedRange=d;b.createRange=function(a){a=a||document;return new d(b.createNativeRange(a))};b.createRangyRange=function(a){a=a||document;return new g(a)};b.createIframeRange=function(a){return b.createRange(e.getIframeDocument(a))};b.createIframeRangyRange=function(a){return b.createRangyRange(e.getIframeDocument(a))};b.addCreateMissingNativeApiListener(function(a){a=a.document;if(typeof a.createRange=="undefined")a.createRange=function(){return b.createRange(this)}; +a=a=null})}); +rangy.createModule("WrappedSelection",function(b,c){function a(a){return(a||window).getSelection()}function d(a){return(a||window).document.selection}function e(a,b,c){var d=c?"end":"start",c=c?"start":"end";a.anchorNode=b[d+"Container"];a.anchorOffset=b[d+"Offset"];a.focusNode=b[c+"Container"];a.focusOffset=b[c+"Offset"]}function f(a){a.anchorNode=a.focusNode=null;a.anchorOffset=a.focusOffset=0;a.rangeCount=0;a.isCollapsed=!0;a._ranges.length=0}function g(a){var c;a instanceof x?(c=a._selectionNativeRange, +c||(c=b.createNativeRange(m.getDocument(a.startContainer)),c.setEnd(a.endContainer,a.endOffset),c.setStart(a.startContainer,a.startOffset),a._selectionNativeRange=c,a.attachListener("detach",function(){this._selectionNativeRange=null}))):a instanceof o?c=a.nativeRange:b.features.implementsDomRange&&a instanceof m.getWindow(a.startContainer).Range&&(c=a);return c}function i(a){var b=a.getNodes(),c;a:if(!b.length||1!=b[0].nodeType)c=!1;else{c=1;for(var d=b.length;c<d;++c)if(!m.isAncestorOf(b[0],b[c])){c= +!1;break a}c=!0}if(!c)throw Error("getSingleElementFromRange: range "+a.inspect()+" did not consist of a single element");return b[0]}function h(a,b){var c=new o(b);a._ranges=[c];e(a,c,!1);a.rangeCount=1;a.isCollapsed=c.collapsed}function k(a){a._ranges.length=0;if("None"==a.docSelection.type)f(a);else{var c=a.docSelection.createRange();if(c&&"undefined"!=typeof c.text)h(a,c);else{a.rangeCount=c.length;for(var d,j=m.getDocument(c.item(0)),k=0;k<a.rangeCount;++k)d=b.createRange(j),d.selectNode(c.item(k)), +a._ranges.push(d);a.isCollapsed=1==a.rangeCount&&a._ranges[0].collapsed;e(a,a._ranges[a.rangeCount-1],!1)}}}function j(a,b){for(var c=a.docSelection.createRange(),d=i(b),e=m.getDocument(c.item(0)),e=m.getBody(e).createControlRange(),h=0,j=c.length;h<j;++h)e.add(c.item(h));try{e.add(d)}catch(f){throw Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");}e.select();k(a)}function n(a,b,c){this.nativeSelection=a;this.docSelection=b;this._ranges= +[];this.win=c;this.refresh()}function p(a,b){for(var c=m.getDocument(b[0].startContainer),c=m.getBody(c).createControlRange(),d=0,e;d<rangeCount;++d){e=i(b[d]);try{c.add(e)}catch(h){throw Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");}}c.select();k(a)}function q(a,b){if(a.anchorNode&&m.getDocument(a.anchorNode)!==m.getDocument(b))throw new z("WRONG_DOCUMENT_ERR");}function r(a){var b=[],c=new w(a.anchorNode,a.anchorOffset), +d=new w(a.focusNode,a.focusOffset),e="function"==typeof a.getName?a.getName():"Selection";if("undefined"!=typeof a.rangeCount)for(var h=0,j=a.rangeCount;h<j;++h)b[h]=x.inspect(a.getRangeAt(h));return"["+e+"(Ranges: "+b.join(", ")+")(anchor: "+c.inspect()+", focus: "+d.inspect()+"]"}b.requireModules(["DomUtil","DomRange","WrappedRange"]);b.config.checkSelectionRanges=!0;var m=b.dom,s=b.util,x=b.DomRange,o=b.WrappedRange,z=b.DOMException,w=m.DomPosition,y,A,t=b.util.isHostMethod(window,"getSelection"), +v=b.util.isHostObject(document,"selection"),D=v&&(!t||b.config.preferTextRange);D?(y=d,b.isSelectionValid=function(a){var a=(a||window).document,b=a.selection;return"None"!=b.type||m.getDocument(b.createRange().parentElement())==a}):t?(y=a,b.isSelectionValid=function(){return!0}):c.fail("Neither document.selection or window.getSelection() detected.");b.getNativeSelection=y;var t=y(),K=b.createNativeRange(document),F=m.getBody(document),E=s.areHostObjects(t,s.areHostProperties(t,["anchorOffset","focusOffset"])); +b.features.selectionHasAnchorAndFocus=E;var I=s.isHostMethod(t,"extend");b.features.selectionHasExtend=I;var N="number"==typeof t.rangeCount;b.features.selectionHasRangeCount=N;var J=!1,C=!0;s.areHostMethods(t,["addRange","getRangeAt","removeAllRanges"])&&("number"==typeof t.rangeCount&&b.features.implementsDomRange)&&function(){var a=document.createElement("iframe");F.appendChild(a);var b=m.getIframeDocument(a);b.open();b.write("<html><head></head><body>12</body></html>");b.close();var c=m.getIframeWindow(a).getSelection(), +d=b.documentElement.lastChild.firstChild,b=b.createRange();b.setStart(d,1);b.collapse(true);c.addRange(b);C=c.rangeCount==1;c.removeAllRanges();var e=b.cloneRange();b.setStart(d,0);e.setEnd(d,2);c.addRange(b);c.addRange(e);J=c.rangeCount==2;b.detach();e.detach();F.removeChild(a)}();b.features.selectionSupportsMultipleRanges=J;b.features.collapsedNonEditableSelectionsSupported=C;var l=!1,u;F&&s.isHostMethod(F,"createControlRange")&&(u=F.createControlRange(),s.areHostProperties(u,["item","add"])&&(l= +!0));b.features.implementsControlRange=l;A=E?function(a){return a.anchorNode===a.focusNode&&a.anchorOffset===a.focusOffset}:function(a){return a.rangeCount?a.getRangeAt(a.rangeCount-1).collapsed:false};var B;s.isHostMethod(t,"getRangeAt")?B=function(a,b){try{return a.getRangeAt(b)}catch(c){return null}}:E&&(B=function(a){var c=m.getDocument(a.anchorNode),c=b.createRange(c);c.setStart(a.anchorNode,a.anchorOffset);c.setEnd(a.focusNode,a.focusOffset);if(c.collapsed!==this.isCollapsed){c.setStart(a.focusNode, +a.focusOffset);c.setEnd(a.anchorNode,a.anchorOffset)}return c});b.getSelection=function(a){var a=a||window,b=a._rangySelection,c=y(a),e=v?d(a):null;if(b){b.nativeSelection=c;b.docSelection=e;b.refresh(a)}else{b=new n(c,e,a);a._rangySelection=b}return b};b.getIframeSelection=function(a){return b.getSelection(m.getIframeWindow(a))};u=n.prototype;if(!D&&E&&s.areHostMethods(t,["removeAllRanges","addRange"])){u.removeAllRanges=function(){this.nativeSelection.removeAllRanges();f(this)};var L=function(a, +c){var d=x.getRangeDocument(c),d=b.createRange(d);d.collapseToPoint(c.endContainer,c.endOffset);a.nativeSelection.addRange(g(d));a.nativeSelection.extend(c.startContainer,c.startOffset);a.refresh()};u.addRange=N?function(a,c){if(l&&v&&this.docSelection.type=="Control")j(this,a);else if(c&&I)L(this,a);else{var d;if(J)d=this.rangeCount;else{this.removeAllRanges();d=0}this.nativeSelection.addRange(g(a));this.rangeCount=this.nativeSelection.rangeCount;if(this.rangeCount==d+1){if(b.config.checkSelectionRanges)(d= +B(this.nativeSelection,this.rangeCount-1))&&!x.rangesEqual(d,a)&&(a=new o(d));this._ranges[this.rangeCount-1]=a;e(this,a,H(this.nativeSelection));this.isCollapsed=A(this)}else this.refresh()}}:function(a,b){if(b&&I)L(this,a);else{this.nativeSelection.addRange(g(a));this.refresh()}};u.setRanges=function(a){if(l&&a.length>1)p(this,a);else{this.removeAllRanges();for(var b=0,c=a.length;b<c;++b)this.addRange(a[b])}}}else if(s.isHostMethod(t,"empty")&&s.isHostMethod(K,"select")&&l&&D)u.removeAllRanges= +function(){try{this.docSelection.empty();if(this.docSelection.type!="None"){var a;if(this.anchorNode)a=m.getDocument(this.anchorNode);else if(this.docSelection.type=="Control"){var b=this.docSelection.createRange();b.length&&(a=m.getDocument(b.item(0)).body.createTextRange())}if(a){a.body.createTextRange().select();this.docSelection.empty()}}}catch(c){}f(this)},u.addRange=function(a){if(this.docSelection.type=="Control")j(this,a);else{o.rangeToTextRange(a).select();this._ranges[0]=a;this.rangeCount= +1;this.isCollapsed=this._ranges[0].collapsed;e(this,a,false)}},u.setRanges=function(a){this.removeAllRanges();var b=a.length;b>1?p(this,a):b&&this.addRange(a[0])};else return c.fail("No means of selecting a Range or TextRange was found"),!1;u.getRangeAt=function(a){if(a<0||a>=this.rangeCount)throw new z("INDEX_SIZE_ERR");return this._ranges[a]};var G;if(D)G=function(a){var c;if(b.isSelectionValid(a.win))c=a.docSelection.createRange();else{c=m.getBody(a.win.document).createTextRange();c.collapse(true)}a.docSelection.type== +"Control"?k(a):c&&typeof c.text!="undefined"?h(a,c):f(a)};else if(s.isHostMethod(t,"getRangeAt")&&"number"==typeof t.rangeCount)G=function(a){if(l&&v&&a.docSelection.type=="Control")k(a);else{a._ranges.length=a.rangeCount=a.nativeSelection.rangeCount;if(a.rangeCount){for(var c=0,d=a.rangeCount;c<d;++c)a._ranges[c]=new b.WrappedRange(a.nativeSelection.getRangeAt(c));e(a,a._ranges[a.rangeCount-1],H(a.nativeSelection));a.isCollapsed=A(a)}else f(a)}};else if(E&&"boolean"==typeof t.isCollapsed&&"boolean"== +typeof K.collapsed&&b.features.implementsDomRange)G=function(a){var b;b=a.nativeSelection;if(b.anchorNode){b=B(b,0);a._ranges=[b];a.rangeCount=1;b=a.nativeSelection;a.anchorNode=b.anchorNode;a.anchorOffset=b.anchorOffset;a.focusNode=b.focusNode;a.focusOffset=b.focusOffset;a.isCollapsed=A(a)}else f(a)};else return c.fail("No means of obtaining a Range or TextRange from the user's selection was found"),!1;u.refresh=function(a){var b=a?this._ranges.slice(0):null;G(this);if(a){a=b.length;if(a!=this._ranges.length)return false; +for(;a--;)if(!x.rangesEqual(b[a],this._ranges[a]))return false;return true}};var P=function(a,b){var c=a.getAllRanges(),d=false;a.removeAllRanges();for(var e=0,h=c.length;e<h;++e)d||b!==c[e]?a.addRange(c[e]):d=true;a.rangeCount||f(a)};u.removeRange=l?function(a){if(this.docSelection.type=="Control"){for(var b=this.docSelection.createRange(),a=i(a),c=m.getDocument(b.item(0)),c=m.getBody(c).createControlRange(),d,e=false,h=0,j=b.length;h<j;++h){d=b.item(h);d!==a||e?c.add(b.item(h)):e=true}c.select(); +k(this)}else P(this,a)}:function(a){P(this,a)};var H;!D&&E&&b.features.implementsDomRange?(H=function(a){var b=false;a.anchorNode&&(b=m.comparePoints(a.anchorNode,a.anchorOffset,a.focusNode,a.focusOffset)==1);return b},u.isBackwards=function(){return H(this)}):H=u.isBackwards=function(){return false};u.toString=function(){for(var a=[],b=0,c=this.rangeCount;b<c;++b)a[b]=""+this._ranges[b];return a.join("")};u.collapse=function(a,c){q(this,a);var d=b.createRange(m.getDocument(a));d.collapseToPoint(a, +c);this.removeAllRanges();this.addRange(d);this.isCollapsed=true};u.collapseToStart=function(){if(this.rangeCount){var a=this._ranges[0];this.collapse(a.startContainer,a.startOffset)}else throw new z("INVALID_STATE_ERR");};u.collapseToEnd=function(){if(this.rangeCount){var a=this._ranges[this.rangeCount-1];this.collapse(a.endContainer,a.endOffset)}else throw new z("INVALID_STATE_ERR");};u.selectAllChildren=function(a){q(this,a);var c=b.createRange(m.getDocument(a));c.selectNodeContents(a);this.removeAllRanges(); +this.addRange(c)};u.deleteFromDocument=function(){if(l&&v&&this.docSelection.type=="Control"){for(var a=this.docSelection.createRange(),b;a.length;){b=a.item(0);a.remove(b);b.parentNode.removeChild(b)}this.refresh()}else if(this.rangeCount){a=this.getAllRanges();this.removeAllRanges();b=0;for(var c=a.length;b<c;++b)a[b].deleteContents();this.addRange(a[c-1])}};u.getAllRanges=function(){return this._ranges.slice(0)};u.setSingleRange=function(a){this.setRanges([a])};u.containsNode=function(a,b){for(var c= +0,d=this._ranges.length;c<d;++c)if(this._ranges[c].containsNode(a,b))return true;return false};u.toHtml=function(){var a="";if(this.rangeCount){for(var a=x.getRangeDocument(this._ranges[0]).createElement("div"),b=0,c=this._ranges.length;b<c;++b)a.appendChild(this._ranges[b].cloneContents());a=a.innerHTML}return a};u.getName=function(){return"WrappedSelection"};u.inspect=function(){return r(this)};u.detach=function(){this.win=this.anchorNode=this.focusNode=this.win._rangySelection=null};n.inspect= +r;b.Selection=n;b.selectionPrototype=u;b.addCreateMissingNativeApiListener(function(a){if(typeof a.getSelection=="undefined")a.getSelection=function(){return b.getSelection(this)};a=null})});var Base=function(){}; +Base.extend=function(b,c){var a=Base.prototype.extend;Base._prototyping=!0;var d=new this;a.call(d,b);d.base=function(){};delete Base._prototyping;var e=d.constructor,f=d.constructor=function(){if(!Base._prototyping)if(this._constructing||this.constructor==f)this._constructing=!0,e.apply(this,arguments),delete this._constructing;else if(null!=arguments[0])return(arguments[0].extend||a).call(arguments[0],d)};f.ancestor=this;f.extend=this.extend;f.forEach=this.forEach;f.implement=this.implement;f.prototype= +d;f.toString=this.toString;f.valueOf=function(a){return"object"==a?f:e.valueOf()};a.call(f,c);"function"==typeof f.init&&f.init();return f}; +Base.prototype={extend:function(b,c){if(1<arguments.length){var a=this[b];if(a&&"function"==typeof c&&(!a.valueOf||a.valueOf()!=c.valueOf())&&/\bbase\b/.test(c)){var d=c.valueOf(),c=function(){var b=this.base||Base.prototype.base;this.base=a;var c=d.apply(this,arguments);this.base=b;return c};c.valueOf=function(a){return"object"==a?c:d};c.toString=Base.toString}this[b]=c}else if(b){var e=Base.prototype.extend;!Base._prototyping&&"function"!=typeof this&&(e=this.extend||e);for(var f={toSource:null}, +g=["constructor","toString","valueOf"],i=Base._prototyping?0:1;h=g[i++];)b[h]!=f[h]&&e.call(this,h,b[h]);for(var h in b)f[h]||e.call(this,h,b[h])}return this}}; +Base=Base.extend({constructor:function(b){this.extend(b)}},{ancestor:Object,version:"1.1",forEach:function(b,c,a){for(var d in b)void 0===this.prototype[d]&&c.call(a,b[d],d,b)},implement:function(){for(var b=0;b<arguments.length;b++)if("function"==typeof arguments[b])arguments[b](this.prototype);else this.prototype.extend(arguments[b]);return this},toString:function(){return""+this.valueOf()}}); +wysihtml5.browser=function(){var b=navigator.userAgent,c=document.createElement("div"),a=-1!==b.indexOf("MSIE")&&-1===b.indexOf("Opera"),d=-1!==b.indexOf("Gecko")&&-1===b.indexOf("KHTML"),e=-1!==b.indexOf("AppleWebKit/"),f=-1!==b.indexOf("Chrome/"),g=-1!==b.indexOf("Opera/");return{USER_AGENT:b,supported:function(){var a=this.USER_AGENT.toLowerCase(),b="contentEditable"in c,d=document.execCommand&&document.queryCommandSupported&&document.queryCommandState,e=document.querySelector&&document.querySelectorAll, +a=this.isIos()&&5>(/ipad|iphone|ipod/.test(a)&&a.match(/ os (\d+).+? like mac os x/)||[,0])[1]||-1!==a.indexOf("opera mobi")||-1!==a.indexOf("hpwos/");return b&&d&&e&&!a},isTouchDevice:function(){return this.supportsEvent("touchmove")},isIos:function(){var a=this.USER_AGENT.toLowerCase();return-1!==a.indexOf("webkit")&&-1!==a.indexOf("mobile")},supportsSandboxedIframes:function(){return a},throwsMixedContentWarningWhenIframeSrcIsEmpty:function(){return!("querySelector"in document)},displaysCaretInEmptyContentEditableCorrectly:function(){return!d}, +hasCurrentStyleProperty:function(){return"currentStyle"in c},insertsLineBreaksOnReturn:function(){return d},supportsPlaceholderAttributeOn:function(a){return"placeholder"in a},supportsEvent:function(a){var b;if(!(b="on"+a in c))c.setAttribute("on"+a,"return;"),b="function"===typeof c["on"+a];return b},supportsEventsInIframeCorrectly:function(){return!g},firesOnDropOnlyWhenOnDragOverIsCancelled:function(){return e||d},supportsDataTransfer:function(){try{return e&&(window.Clipboard||window.DataTransfer).prototype.getData}catch(a){return!1}}, +supportsHTML5Tags:function(a){a=a.createElement("div");a.innerHTML="<article>foo</article>";return"<article>foo</article>"===a.innerHTML.toLowerCase()},supportsCommand:function(){var b={formatBlock:a,insertUnorderedList:a||g||e,insertOrderedList:a||g||e},c={insertHTML:d};return function(a,d){if(!b[d]){try{return a.queryCommandSupported(d)}catch(e){}try{return a.queryCommandEnabled(d)}catch(f){return!!c[d]}}return!1}}(),doesAutoLinkingInContentEditable:function(){return a},canDisableAutoLinking:function(){return this.supportsCommand(document, +"AutoUrlDetect")},clearsContentEditableCorrectly:function(){return d||g||e},supportsGetAttributeCorrectly:function(){return"1"!=document.createElement("td").getAttribute("rowspan")},canSelectImagesInContentEditable:function(){return d||a||g},clearsListsInContentEditableCorrectly:function(){return d||a||e},autoScrollsToCaret:function(){return!e},autoClosesUnclosedTags:function(){var a=c.cloneNode(!1),b;a.innerHTML="<p><div></div>";a=a.innerHTML.toLowerCase();b="<p></p><div></div>"===a||"<p><div></div></p>"=== +a;this.autoClosesUnclosedTags=function(){return b};return b},supportsNativeGetElementsByClassName:function(){return-1!==(""+document.getElementsByClassName).indexOf("[native code]")},supportsSelectionModify:function(){return"getSelection"in window&&"modify"in window.getSelection()},supportsClassList:function(){return"classList"in c},needsSpaceAfterLineBreak:function(){return g},supportsSpeechApiOn:function(a){return 11<=(b.match(/Chrome\/(\d+)/)||[,0])[1]&&("onwebkitspeechchange"in a||"speech"in a)}, +crashesWhenDefineProperty:function(b){return a&&("XMLHttpRequest"===b||"XDomainRequest"===b)},doesAsyncFocus:function(){return a},hasProblemsSettingCaretAfterImg:function(){return a},hasUndoInContextMenu:function(){return d||f||g}}}(); +wysihtml5.lang.array=function(b){return{contains:function(c){if(b.indexOf)return-1!==b.indexOf(c);for(var a=0,d=b.length;a<d;a++)if(b[a]===c)return!0;return!1},without:function(c){for(var c=wysihtml5.lang.array(c),a=[],d=0,e=b.length;d<e;d++)c.contains(b[d])||a.push(b[d]);return a},get:function(){for(var c=0,a=b.length,d=[];c<a;c++)d.push(b[c]);return d}}}; +wysihtml5.lang.Dispatcher=Base.extend({observe:function(b,c){this.events=this.events||{};this.events[b]=this.events[b]||[];this.events[b].push(c);return this},on:function(){return this.observe.apply(this,wysihtml5.lang.array(arguments).get())},fire:function(b,c){this.events=this.events||{};for(var a=this.events[b]||[],d=0;d<a.length;d++)a[d].call(this,c);return this},stopObserving:function(b,c){this.events=this.events||{};var a=0,d,e;if(b){d=this.events[b]||[];for(e=[];a<d.length;a++)d[a]!==c&&c&& +e.push(d[a]);this.events[b]=e}else this.events={};return this}});wysihtml5.lang.object=function(b){return{merge:function(c){for(var a in c)b[a]=c[a];return this},get:function(){return b},clone:function(){var c={},a;for(a in b)c[a]=b[a];return c},isArray:function(){return"[object Array]"===Object.prototype.toString.call(b)}}}; +(function(){var b=/^\s+/,c=/\s+$/;wysihtml5.lang.string=function(a){a=""+a;return{trim:function(){return a.replace(b,"").replace(c,"")},interpolate:function(b){for(var c in b)a=this.replace("#{"+c+"}").by(b[c]);return a},replace:function(b){return{by:function(c){return a.split(b).join(c)}}}}}})(); +(function(b){function c(a){return a.replace(e,function(a,b){var c=(b.match(f)||[])[1]||"",d=i[c],b=b.replace(f,"");b.split(d).length>b.split(c).length&&(b+=c,c="");var e=d=b;b.length>g&&(e=e.substr(0,g)+"...");"www."===d.substr(0,4)&&(d="http://"+d);return'<a href="'+d+'">'+e+"</a>"+c})}function a(h){if(!d.contains(h.nodeName))if(h.nodeType===b.TEXT_NODE&&h.data.match(e)){var f=h.parentNode,j;j=f.ownerDocument;var g=j._wysihtml5_tempElement;g||(g=j._wysihtml5_tempElement=j.createElement("div"));j= +g;j.innerHTML="<span></span>"+c(h.data);for(j.removeChild(j.firstChild);j.firstChild;)f.insertBefore(j.firstChild,h);f.removeChild(h)}else{f=b.lang.array(h.childNodes).get();j=f.length;for(g=0;g<j;g++)a(f[g]);return h}}var d=b.lang.array("CODE PRE A SCRIPT HEAD TITLE STYLE".split(" ")),e=/((https?:\/\/|www\.)[^\s<]{3,})/gi,f=/([^\w\/\-](,?))$/i,g=100,i={")":"(","]":"[","}":"{"};b.dom.autoLink=function(b){var c;a:{c=b;for(var e;c.parentNode;){c=c.parentNode;e=c.nodeName;if(d.contains(e)){c=!0;break a}if("body"=== +e)break}c=!1}if(c)return b;b===b.ownerDocument.documentElement&&(b=b.ownerDocument.body);return a(b)};b.dom.autoLink.URL_REG_EXP=e})(wysihtml5); +(function(b){var c=b.browser.supportsClassList(),a=b.dom;a.addClass=function(b,e){if(c)return b.classList.add(e);a.hasClass(b,e)||(b.className+=" "+e)};a.removeClass=function(a,b){if(c)return a.classList.remove(b);a.className=a.className.replace(RegExp("(^|\\s+)"+b+"(\\s+|$)")," ")};a.hasClass=function(a,b){if(c)return a.classList.contains(b);var f=a.className;return 0<f.length&&(f==b||RegExp("(^|\\s)"+b+"(\\s|$)").test(f))}})(wysihtml5); +wysihtml5.dom.contains=function(){var b=document.documentElement;if(b.contains)return function(b,a){a.nodeType!==wysihtml5.ELEMENT_NODE&&(a=a.parentNode);return b!==a&&b.contains(a)};if(b.compareDocumentPosition)return function(b,a){return!!(b.compareDocumentPosition(a)&16)}}(); +wysihtml5.dom.convertToList=function(){function b(b,a){var d=b.createElement("li");a.appendChild(d);return d}return function(c,a){if("UL"===c.nodeName||"OL"===c.nodeName||"MENU"===c.nodeName)return c;var d=c.ownerDocument,e=d.createElement(a),f=c.querySelectorAll("br"),g=f.length,i,h,k,j,n;for(n=0;n<g;n++)for(i=f[n];(h=i.parentNode)&&h!==c&&h.lastChild===i;){if("block"===wysihtml5.dom.getStyle("display").from(h)){h.removeChild(i);break}wysihtml5.dom.insert(i).after(i.parentNode)}f=wysihtml5.lang.array(c.childNodes).get(); +g=f.length;for(n=0;n<g;n++)j=j||b(d,e),i=f[n],h="block"===wysihtml5.dom.getStyle("display").from(i),k="BR"===i.nodeName,h?(j=j.firstChild?b(d,e):j,j.appendChild(i),j=null):k?j=j.firstChild?null:j:j.appendChild(i);c.parentNode.replaceChild(e,c);return e}}();wysihtml5.dom.copyAttributes=function(b){return{from:function(c){return{to:function(a){for(var d,e=0,f=b.length;e<f;e++)d=b[e],"undefined"!==typeof c[d]&&""!==c[d]&&(a[d]=c[d]);return{andTo:arguments.callee}}}}}}; +(function(b){var c=["-webkit-box-sizing","-moz-box-sizing","-ms-box-sizing","box-sizing"],a=function(a){var e;a:for(var f=0,g=c.length;f<g;f++)if("border-box"===b.getStyle(c[f]).from(a)){e=c[f];break a}return e?parseInt(b.getStyle("width").from(a),10)<a.offsetWidth:!1};b.copyStyles=function(d){return{from:function(e){a(e)&&(d=wysihtml5.lang.array(d).without(c));for(var f="",g=d.length,i=0,h;i<g;i++)h=d[i],f+=h+":"+b.getStyle(h).from(e)+";";return{to:function(a){b.setStyles(f).on(a);return{andTo:arguments.callee}}}}}}})(wysihtml5.dom); +(function(b){b.dom.delegate=function(c,a,d,e){return b.dom.observe(c,d,function(d){for(var g=d.target,i=b.lang.array(c.querySelectorAll(a));g&&g!==c;){if(i.contains(g)){e.call(g,d);break}g=g.parentNode}})}})(wysihtml5); +wysihtml5.dom.getAsDom=function(){var b="abbr article aside audio bdi canvas command datalist details figcaption figure footer header hgroup keygen mark meter nav output progress rp rt ruby svg section source summary time track video wbr".split(" ");return function(c,a){var a=a||document,d;if("object"===typeof c&&c.nodeType)d=a.createElement("div"),d.appendChild(c);else if(wysihtml5.browser.supportsHTML5Tags(a))d=a.createElement("div"),d.innerHTML=c;else{d=a;if(!d._wysihtml5_supportsHTML5Tags){for(var e= +0,f=b.length;e<f;e++)d.createElement(b[e]);d._wysihtml5_supportsHTML5Tags=!0}d=a;e=d.createElement("div");e.style.display="none";d.body.appendChild(e);try{e.innerHTML=c}catch(g){}d.body.removeChild(e);d=e}return d}}(); +wysihtml5.dom.getParentElement=function(){function b(b,a){return!a||!a.length?!0:"string"===typeof a?b===a:wysihtml5.lang.array(a).contains(b)}return function(c,a,d){d=d||50;if(a.className||a.classRegExp){a:{for(var e=a.nodeName,f=a.className,a=a.classRegExp;d--&&c&&"BODY"!==c.nodeName;){var g;if(g=c.nodeType===wysihtml5.ELEMENT_NODE)if(g=b(c.nodeName,e)){g=f;var i=(c.className||"").match(a)||[];g=!g?!!i.length:i[i.length-1]===g}if(g)break a;c=c.parentNode}c=null}return c}a:{e=a.nodeName;for(f=d;f--&& +c&&"BODY"!==c.nodeName;){if(b(c.nodeName,e))break a;c=c.parentNode}c=null}return c}}(); +wysihtml5.dom.getStyle=function(){function b(b){return b.replace(a,function(a){return a.charAt(1).toUpperCase()})}var c={"float":"styleFloat"in document.createElement("div").style?"styleFloat":"cssFloat"},a=/\-[a-z]/g;return function(a){return{from:function(e){if(e.nodeType===wysihtml5.ELEMENT_NODE){var f=e.ownerDocument,g=c[a]||b(a),i=e.style,h=e.currentStyle,k=i[g];if(k)return k;if(h)try{return h[g]}catch(j){}var g=f.defaultView||f.parentWindow,f=("height"===a||"width"===a)&&"TEXTAREA"===e.nodeName, +n;if(g.getComputedStyle)return f&&(n=i.overflow,i.overflow="hidden"),e=g.getComputedStyle(e,null).getPropertyValue(a),f&&(i.overflow=n||""),e}}}}}();wysihtml5.dom.hasElementWithTagName=function(){var b={},c=1;return function(a,d){var e=(a._wysihtml5_identifier||(a._wysihtml5_identifier=c++))+":"+d,f=b[e];f||(f=b[e]=a.getElementsByTagName(d));return 0<f.length}}(); +(function(b){var c={},a=1;b.dom.hasElementWithClassName=function(d,e){if(!b.browser.supportsNativeGetElementsByClassName())return!!d.querySelector("."+e);var f=(d._wysihtml5_identifier||(d._wysihtml5_identifier=a++))+":"+e,g=c[f];g||(g=c[f]=d.getElementsByClassName(e));return 0<g.length}})(wysihtml5);wysihtml5.dom.insert=function(b){return{after:function(c){c.parentNode.insertBefore(b,c.nextSibling)},before:function(c){c.parentNode.insertBefore(b,c)},into:function(c){c.appendChild(b)}}}; +wysihtml5.dom.insertCSS=function(b){b=b.join("\n");return{into:function(c){var a=c.head||c.getElementsByTagName("head")[0],d=c.createElement("style");d.type="text/css";d.styleSheet?d.styleSheet.cssText=b:d.appendChild(c.createTextNode(b));a&&a.appendChild(d)}}}; +wysihtml5.dom.observe=function(b,c,a){for(var c="string"===typeof c?[c]:c,d,e,f=0,g=c.length;f<g;f++)e=c[f],b.addEventListener?b.addEventListener(e,a,!1):(d=function(c){"target"in c||(c.target=c.srcElement);c.preventDefault=c.preventDefault||function(){this.returnValue=false};c.stopPropagation=c.stopPropagation||function(){this.cancelBubble=true};a.call(b,c)},b.attachEvent("on"+e,d));return{stop:function(){for(var e,h=0,f=c.length;h<f;h++)e=c[h],b.removeEventListener?b.removeEventListener(e,a,!1): +b.detachEvent("on"+e,d)}}}; +wysihtml5.dom.parse=function(){function b(c,e){var h=c.childNodes,f=h.length,g;g=a[c.nodeType];var k=0;g=g&&g(c);if(!g)return null;for(k=0;k<f;k++)(newChild=b(h[k],e))&&g.appendChild(newChild);return e&&1>=g.childNodes.length&&g.nodeName.toLowerCase()===d&&!g.attributes.length?g.firstChild:g}function c(a,b){var b=b.toLowerCase(),c;if(c="IMG"==a.nodeName)if(c="src"==b){var d;try{d=a.complete&&!a.mozMatchesSelector(":-moz-broken")}catch(e){a.complete&&"complete"===a.readyState&&(d=!0)}c=!0===d}return c? +a.src:i&&"outerHTML"in a?-1!=a.outerHTML.toLowerCase().indexOf(" "+b+"=")?a.getAttribute(b):null:a.getAttribute(b)}var a={1:function(a){var b,f,i=g.tags;f=a.nodeName.toLowerCase();b=a.scopeName;if(a._wysihtml5)return null;a._wysihtml5=1;if("wysihtml5-temp"===a.className)return null;b&&"HTML"!=b&&(f=b+":"+f);"outerHTML"in a&&!wysihtml5.browser.autoClosesUnclosedTags()&&("P"===a.nodeName&&"</p>"!==a.outerHTML.slice(-4).toLowerCase())&&(f="div");if(f in i){b=i[f];if(!b||b.remove)return null;b="string"=== +typeof b?{rename_tag:b}:b}else if(a.firstChild)b={rename_tag:d};else return null;f=a.ownerDocument.createElement(b.rename_tag||f);var i={},r=b.set_class,m=b.add_class,s=b.set_attributes,x=b.check_attributes,o=g.classes,z=0,w=[];b=[];var y=[],A=[],t;s&&(i=wysihtml5.lang.object(s).clone());if(x)for(t in x)if(s=h[x[t]])s=s(c(a,t)),"string"===typeof s&&(i[t]=s);r&&w.push(r);if(m)for(t in m)if(s=k[m[t]])r=s(c(a,t)),"string"===typeof r&&w.push(r);o["_wysihtml5-temp-placeholder"]=1;(A=a.getAttribute("class"))&& +(w=w.concat(A.split(e)));for(m=w.length;z<m;z++)a=w[z],o[a]&&b.push(a);for(o=b.length;o--;)a=b[o],wysihtml5.lang.array(y).contains(a)||y.unshift(a);y.length&&(i["class"]=y.join(" "));for(t in i)try{f.setAttribute(t,i[t])}catch(v){}i.src&&("undefined"!==typeof i.width&&f.setAttribute("width",i.width),"undefined"!==typeof i.height&&f.setAttribute("height",i.height));return f},3:function(a){return a.ownerDocument.createTextNode(a.data)}},d="span",e=/\s+/,f={tags:{},classes:{}},g={},i=!wysihtml5.browser.supportsGetAttributeCorrectly(), +h={url:function(){var a=/^https?:\/\//i;return function(b){return!b||!b.match(a)?null:b.replace(a,function(a){return a.toLowerCase()})}}(),alt:function(){var a=/[^ a-z0-9_\-]/gi;return function(b){return!b?"":b.replace(a,"")}}(),numbers:function(){var a=/\D/g;return function(b){return(b=(b||"").replace(a,""))||null}}()},k={align_img:function(){var a={left:"wysiwyg-float-left",right:"wysiwyg-float-right"};return function(b){return a[(""+b).toLowerCase()]}}(),align_text:function(){var a={left:"wysiwyg-text-align-left", +right:"wysiwyg-text-align-right",center:"wysiwyg-text-align-center",justify:"wysiwyg-text-align-justify"};return function(b){return a[(""+b).toLowerCase()]}}(),clear_br:function(){var a={left:"wysiwyg-clear-left",right:"wysiwyg-clear-right",both:"wysiwyg-clear-both",all:"wysiwyg-clear-both"};return function(b){return a[(""+b).toLowerCase()]}}(),size_font:function(){var a={1:"wysiwyg-font-size-xx-small",2:"wysiwyg-font-size-small",3:"wysiwyg-font-size-medium",4:"wysiwyg-font-size-large",5:"wysiwyg-font-size-x-large", +6:"wysiwyg-font-size-xx-large",7:"wysiwyg-font-size-xx-large","-":"wysiwyg-font-size-smaller","+":"wysiwyg-font-size-larger"};return function(b){return a[(""+b).charAt(0)]}}()};return function(a,c,d,e){wysihtml5.lang.object(g).merge(f).merge(c).get();for(var d=d||a.ownerDocument||document,c=d.createDocumentFragment(),h="string"===typeof a,a=h?wysihtml5.dom.getAsDom(a,d):a;a.firstChild;)d=a.firstChild,a.removeChild(d),(d=b(d,e))&&c.appendChild(d);a.innerHTML="";a.appendChild(c);return h?wysihtml5.quirks.getCorrectInnerHTML(a): +a}}();wysihtml5.dom.removeEmptyTextNodes=function(b){for(var c=wysihtml5.lang.array(b.childNodes).get(),a=c.length,d=0;d<a;d++)b=c[d],b.nodeType===wysihtml5.TEXT_NODE&&""===b.data&&b.parentNode.removeChild(b)};wysihtml5.dom.renameElement=function(b,c){for(var a=b.ownerDocument.createElement(c),d;d=b.firstChild;)a.appendChild(d);wysihtml5.dom.copyAttributes(["align","className"]).from(b).to(a);b.parentNode.replaceChild(a,b);return a}; +wysihtml5.dom.replaceWithChildNodes=function(b){if(b.parentNode)if(b.firstChild){for(var c=b.ownerDocument.createDocumentFragment();b.firstChild;)c.appendChild(b.firstChild);b.parentNode.replaceChild(c,b)}else b.parentNode.removeChild(b)}; +(function(b){function c(a){var b=a.ownerDocument.createElement("br");a.appendChild(b)}b.resolveList=function(a){if(!("MENU"!==a.nodeName&&"UL"!==a.nodeName&&"OL"!==a.nodeName)){var d=a.ownerDocument.createDocumentFragment(),e=a.previousElementSibling||a.previousSibling,f,g,i;for(e&&"block"!==b.getStyle("display").from(e)&&c(d);i=a.firstChild;){for(f=i.lastChild;e=i.firstChild;)g=(g=e===f)&&"block"!==b.getStyle("display").from(e)&&"BR"!==e.nodeName,d.appendChild(e),g&&c(d);i.parentNode.removeChild(i)}a.parentNode.replaceChild(d, +a)}}})(wysihtml5.dom); +(function(b){var c=document,a="parent top opener frameElement frames localStorage globalStorage sessionStorage indexedDB".split(" "),d="open close openDialog showModalDialog alert confirm prompt openDatabase postMessage XMLHttpRequest XDomainRequest".split(" "),e=["referrer","write","open","close"];b.dom.Sandbox=Base.extend({constructor:function(a,c){this.callback=a||b.EMPTY_FUNCTION;this.config=b.lang.object({}).merge(c).get();this.iframe=this._createIframe()},insertInto:function(a){"string"===typeof a&& +(a=c.getElementById(a));a.appendChild(this.iframe)},getIframe:function(){return this.iframe},getWindow:function(){this._readyError()},getDocument:function(){this._readyError()},destroy:function(){var a=this.getIframe();a.parentNode.removeChild(a)},_readyError:function(){throw Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");},_createIframe:function(){var a=this,d=c.createElement("iframe");d.className="wysihtml5-sandbox";b.dom.setAttributes({security:"restricted",allowtransparency:"true", +frameborder:0,width:0,height:0,marginwidth:0,marginheight:0}).on(d);b.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()&&(d.src="javascript:'<html></html>'");d.onload=function(){d.onreadystatechange=d.onload=null;a._onLoadIframe(d)};d.onreadystatechange=function(){if(/loaded|complete/.test(d.readyState)){d.onreadystatechange=d.onload=null;a._onLoadIframe(d)}};return d},_onLoadIframe:function(f){if(b.dom.contains(c.documentElement,f)){var g=this,i=f.contentWindow,h=f.contentWindow.document,k= +this._getHtml({charset:c.characterSet||c.charset||"utf-8",stylesheets:this.config.stylesheets});h.open("text/html","replace");h.write(k);h.close();this.getWindow=function(){return f.contentWindow};this.getDocument=function(){return f.contentWindow.document};i.onerror=function(a,b,c){throw Error("wysihtml5.Sandbox: "+a,b,c);};if(!b.browser.supportsSandboxedIframes()){var j,k=0;for(j=a.length;k<j;k++)this._unset(i,a[k]);k=0;for(j=d.length;k<j;k++)this._unset(i,d[k],b.EMPTY_FUNCTION);k=0;for(j=e.length;k< +j;k++)this._unset(h,e[k]);this._unset(h,"cookie","",!0)}this.loaded=!0;setTimeout(function(){g.callback(g)},0)}},_getHtml:function(a){var c=a.stylesheets,d="",e=0,k;if(c="string"===typeof c?[c]:c)for(k=c.length;e<k;e++)d+='<link rel="stylesheet" href="'+c[e]+'">';a.stylesheets=d;return b.lang.string('<!DOCTYPE html><html><head><meta charset="#{charset}">#{stylesheets}</head><body></body></html>').interpolate(a)},_unset:function(a,c,d,e){try{a[c]=d}catch(k){}try{a.__defineGetter__(c,function(){return d})}catch(j){}if(e)try{a.__defineSetter__(c, +function(){})}catch(n){}if(!b.browser.crashesWhenDefineProperty(c))try{var p={get:function(){return d}};e&&(p.set=function(){});Object.defineProperty(a,c,p)}catch(q){}}})})(wysihtml5);(function(){var b={className:"class"};wysihtml5.dom.setAttributes=function(c){return{on:function(a){for(var d in c)a.setAttribute(b[d]||d,c[d])}}}})(); +wysihtml5.dom.setStyles=function(b){return{on:function(c){c=c.style;if("string"===typeof b)c.cssText+=";"+b;else for(var a in b)"float"===a?(c.cssFloat=b[a],c.styleFloat=b[a]):c[a]=b[a]}}}; +(function(b){b.simulatePlaceholder=function(c,a,d){var e=function(){a.hasPlaceholderSet()&&a.clear();b.removeClass(a.element,"placeholder")},f=function(){a.isEmpty()&&(a.setValue(d),b.addClass(a.element,"placeholder"))};c.observe("set_placeholder",f).observe("unset_placeholder",e).observe("focus:composer",e).observe("paste:composer",e).observe("blur:composer",f);f()}})(wysihtml5.dom); +(function(b){var c=document.documentElement;"textContent"in c?(b.setTextContent=function(a,b){a.textContent=b},b.getTextContent=function(a){return a.textContent}):"innerText"in c?(b.setTextContent=function(a,b){a.innerText=b},b.getTextContent=function(a){return a.innerText}):(b.setTextContent=function(a,b){a.nodeValue=b},b.getTextContent=function(a){return a.nodeValue})})(wysihtml5.dom); +wysihtml5.quirks.cleanPastedHTML=function(){var b={"a u":wysihtml5.dom.replaceWithChildNodes};return function(c,a,d){var a=a||b,d=d||c.ownerDocument||document,e="string"===typeof c,f,g,i,h=0,c=e?wysihtml5.dom.getAsDom(c,d):c;for(i in a){f=c.querySelectorAll(i);d=a[i];for(g=f.length;h<g;h++)d(f[h])}return e?c.innerHTML:c}}(); +(function(b){var c=b.dom;b.quirks.ensureProperClearing=function(){var a=function(){var a=this;setTimeout(function(){var b=a.innerHTML.toLowerCase();if("<p> </p>"==b||"<p> </p><p> </p>"==b)a.innerHTML=""},0)};return function(b){c.observe(b.element,["cut","keydown"],a)}}();b.quirks.ensureProperClearingOfLists=function(){var a=["OL","UL","MENU"];return function(d){c.observe(d.element,"keydown",function(e){if(e.keyCode===b.BACKSPACE_KEY){var f=d.selection.getSelectedNode(),e=d.element; +e.firstChild&&b.lang.array(a).contains(e.firstChild.nodeName)&&(f=c.getParentElement(f,{nodeName:a}))&&f==e.firstChild&&1>=f.childNodes.length&&(f.firstChild?""===f.firstChild.innerHTML:1)&&f.parentNode.removeChild(f)}})}}()})(wysihtml5); +(function(b){b.quirks.getCorrectInnerHTML=function(c){var a=c.innerHTML;if(-1===a.indexOf("%7E"))return a;var c=c.querySelectorAll("[href*='~'], [src*='~']"),d,e,f,g;g=0;for(f=c.length;g<f;g++)d=c[g].href||c[g].src,e=b.lang.string(d).replace("~").by("%7E"),a=b.lang.string(a).replace(e).by(d);return a}})(wysihtml5); +(function(b){var c=b.dom,a="LI P H1 H2 H3 H4 H5 H6".split(" "),d=["UL","OL","MENU"];b.quirks.insertLineBreakOnReturn=function(e){function f(a){if(a=c.getParentElement(a,{nodeName:["P","DIV"]},2)){var d=document.createTextNode(b.INVISIBLE_SPACE);c.insert(d).before(a);c.replaceWithChildNodes(a);e.selection.selectNode(d)}}c.observe(e.element.ownerDocument,"keydown",function(g){var i=g.keyCode;if(!(g.shiftKey||i!==b.ENTER_KEY&&i!==b.BACKSPACE_KEY)){var h=e.selection.getSelectedNode();(h=c.getParentElement(h, +{nodeName:a},4))?"LI"===h.nodeName&&(i===b.ENTER_KEY||i===b.BACKSPACE_KEY)?setTimeout(function(){var a=e.selection.getSelectedNode(),b;a&&((b=c.getParentElement(a,{nodeName:d},2))||f(a))},0):h.nodeName.match(/H[1-6]/)&&i===b.ENTER_KEY&&setTimeout(function(){f(e.selection.getSelectedNode())},0):i===b.ENTER_KEY&&!b.browser.insertsLineBreaksOnReturn()&&(e.commands.exec("insertLineBreak"),g.preventDefault())}})}})(wysihtml5); +(function(b){b.quirks.redraw=function(c){b.dom.addClass(c,"wysihtml5-quirks-redraw");b.dom.removeClass(c,"wysihtml5-quirks-redraw");try{var a=c.ownerDocument;a.execCommand("italic",!1,null);a.execCommand("italic",!1,null)}catch(d){}}})(wysihtml5); +(function(b){var c=b.dom;b.Selection=Base.extend({constructor:function(a){window.rangy.init();this.editor=a;this.composer=a.composer;this.doc=this.composer.doc},getBookmark:function(){var a=this.getRange();return a&&a.cloneRange()},setBookmark:function(a){a&&this.setSelection(a)},setBefore:function(a){var b=rangy.createRange(this.doc);b.setStartBefore(a);b.setEndBefore(a);return this.setSelection(b)},setAfter:function(a){var b=rangy.createRange(this.doc);b.setStartAfter(a);b.setEndAfter(a);return this.setSelection(b)}, +selectNode:function(a){var d=rangy.createRange(this.doc),e=a.nodeType===b.ELEMENT_NODE,f="canHaveHTML"in a?a.canHaveHTML:"IMG"!==a.nodeName,g=e?a.innerHTML:a.data,g=""===g||g===b.INVISIBLE_SPACE,i=c.getStyle("display").from(a),i="block"===i||"list-item"===i;if(g&&e&&f)try{a.innerHTML=b.INVISIBLE_SPACE}catch(h){}f?d.selectNodeContents(a):d.selectNode(a);f&&g&&e?d.collapse(i):f&&g&&(d.setStartAfter(a),d.setEndAfter(a));this.setSelection(d)},getSelectedNode:function(a){if(a&&this.doc.selection&&"Control"=== +this.doc.selection.type&&(a=this.doc.selection.createRange())&&a.length)return a.item(0);a=this.getSelection(this.doc);return a.focusNode===a.anchorNode?a.focusNode:(a=this.getRange(this.doc))?a.commonAncestorContainer:this.doc.body},executeAndRestore:function(a,c){var e=this.doc.body,f=c&&e.scrollTop,g=c&&e.scrollLeft,i='<span class="_wysihtml5-temp-placeholder">'+b.INVISIBLE_SPACE+"</span>",h=this.getRange(this.doc);if(h){i=h.createContextualFragment(i);h.insertNode(i);try{a(h.startContainer,h.endContainer)}catch(k){setTimeout(function(){throw k; +},0)}(caretPlaceholder=this.doc.querySelector("._wysihtml5-temp-placeholder"))?(h=rangy.createRange(this.doc),h.selectNode(caretPlaceholder),h.deleteContents(),this.setSelection(h)):e.focus();c&&(e.scrollTop=f,e.scrollLeft=g);try{caretPlaceholder.parentNode.removeChild(caretPlaceholder)}catch(j){}}else a(e,e)},executeAndRestoreSimple:function(a){var b,c,f=this.getRange(),g=this.doc.body,i;if(f){b=f.getNodes([3]);g=b[0]||f.startContainer;i=b[b.length-1]||f.endContainer;b=g===f.startContainer?f.startOffset: +0;c=i===f.endContainer?f.endOffset:i.length;try{a(f.startContainer,f.endContainer)}catch(h){setTimeout(function(){throw h;},0)}a=rangy.createRange(this.doc);try{a.setStart(g,b)}catch(k){}try{a.setEnd(i,c)}catch(j){}try{this.setSelection(a)}catch(n){}}else a(g,g)},insertHTML:function(a){var a=rangy.createRange(this.doc).createContextualFragment(a),b=a.lastChild;this.insertNode(a);b&&this.setAfter(b)},insertNode:function(a){var b=this.getRange();b&&b.insertNode(a)},surround:function(a){var b=this.getRange(); +if(b)try{b.surroundContents(a),this.selectNode(a)}catch(c){a.appendChild(b.extractContents()),b.insertNode(a)}},scrollIntoView:function(){var a=this.doc,c=a.documentElement.scrollHeight>a.documentElement.offsetHeight,e;if(!(e=a._wysihtml5ScrollIntoViewElement))e=a.createElement("span"),e.innerHTML=b.INVISIBLE_SPACE;e=a._wysihtml5ScrollIntoViewElement=e;if(c){this.insertNode(e);var c=e,f=0;if(c.parentNode){do f+=c.offsetTop||0,c=c.offsetParent;while(c)}c=f;e.parentNode.removeChild(e);c>a.body.scrollTop&& +(a.body.scrollTop=c)}},selectLine:function(){b.browser.supportsSelectionModify()?this._selectLine_W3C():this.doc.selection&&this._selectLine_MSIE()},_selectLine_W3C:function(){var a=this.doc.defaultView.getSelection();a.modify("extend","left","lineboundary");a.modify("extend","right","lineboundary")},_selectLine_MSIE:function(){var a=this.doc.selection.createRange(),b=a.boundingTop,c=this.doc.body.scrollWidth,f;if(a.moveToPoint){0===b&&(f=this.doc.createElement("span"),this.insertNode(f),b=f.offsetTop, +f.parentNode.removeChild(f));b+=1;for(f=-10;f<c;f+=2)try{a.moveToPoint(f,b);break}catch(g){}for(f=this.doc.selection.createRange();0<=c;c--)try{f.moveToPoint(c,b);break}catch(i){}a.setEndPoint("EndToEnd",f);a.select()}},getText:function(){var a=this.getSelection();return a?a.toString():""},getNodes:function(a,b){var c=this.getRange();return c?c.getNodes([a],b):[]},getRange:function(){var a=this.getSelection();return a&&a.rangeCount&&a.getRangeAt(0)},getSelection:function(){return rangy.getSelection(this.doc.defaultView|| +this.doc.parentWindow)},setSelection:function(a){return rangy.getSelection(this.doc.defaultView||this.doc.parentWindow).setSingleRange(a)}})})(wysihtml5); +(function(b,c){function a(a,b){return c.dom.isCharacterDataNode(a)?0==b?!!a.previousSibling:b==a.length?!!a.nextSibling:!0:0<b&&b<a.childNodes.length}function d(a,b,e){var f;c.dom.isCharacterDataNode(b)&&(0==e?(e=c.dom.getNodeIndex(b),b=b.parentNode):e==b.length?(e=c.dom.getNodeIndex(b)+1,b=b.parentNode):f=c.dom.splitDataNode(b,e));if(!f){f=b.cloneNode(!1);f.id&&f.removeAttribute("id");for(var g;g=b.childNodes[e];)f.appendChild(g);c.dom.insertAfter(f,b)}return b==a?f:d(a,f.parentNode,c.dom.getNodeIndex(f))} +function e(a){this.firstTextNode=(this.isElementMerge=a.nodeType==b.ELEMENT_NODE)?a.lastChild:a;this.textNodes=[this.firstTextNode]}function f(a,b,c,d){this.tagNames=a||[g];this.cssClass=b||"";this.similarClassRegExp=c;this.normalize=d;this.applyToAnyTagName=!1}var g="span",i=/\s+/g;e.prototype={doMerge:function(){for(var a=[],b,c,d=0,e=this.textNodes.length;d<e;++d)b=this.textNodes[d],c=b.parentNode,a[d]=b.data,d&&(c.removeChild(b),c.hasChildNodes()||c.parentNode.removeChild(c));return this.firstTextNode.data= +a=a.join("")},getLength:function(){for(var a=this.textNodes.length,b=0;a--;)b+=this.textNodes[a].length;return b},toString:function(){for(var a=[],b=0,c=this.textNodes.length;b<c;++b)a[b]="'"+this.textNodes[b].data+"'";return"[Merge("+a.join(",")+")]"}};f.prototype={getAncestorWithClass:function(a){for(var d;a;){if(this.cssClass)if(d=this.cssClass,a.className){var e=a.className.match(this.similarClassRegExp)||[];d=e[e.length-1]===d}else d=!1;else d=!0;if(a.nodeType==b.ELEMENT_NODE&&c.dom.arrayContains(this.tagNames, +a.tagName.toLowerCase())&&d)return a;a=a.parentNode}return!1},postApply:function(a,b){for(var c=a[0],d=a[a.length-1],f=[],g,i=c,m=d,s=0,x=d.length,o,z,w=0,y=a.length;w<y;++w)if(o=a[w],z=this.getAdjacentMergeableTextNode(o.parentNode,!1)){if(g||(g=new e(z),f.push(g)),g.textNodes.push(o),o===c&&(i=g.firstTextNode,s=i.length),o===d)m=g.firstTextNode,x=g.getLength()}else g=null;if(c=this.getAdjacentMergeableTextNode(d.parentNode,!0))g||(g=new e(d),f.push(g)),g.textNodes.push(c);if(f.length){w=0;for(y= +f.length;w<y;++w)f[w].doMerge();b.setStart(i,s);b.setEnd(m,x)}},getAdjacentMergeableTextNode:function(a,c){var d=a.nodeType==b.TEXT_NODE,e=d?a.parentNode:a,f=c?"nextSibling":"previousSibling";if(d){if((d=a[f])&&d.nodeType==b.TEXT_NODE)return d}else if((d=e[f])&&this.areElementsMergeable(a,d))return d[c?"firstChild":"lastChild"];return null},areElementsMergeable:function(a,b){var d;if(d=c.dom.arrayContains(this.tagNames,(a.tagName||"").toLowerCase()))if(d=c.dom.arrayContains(this.tagNames,(b.tagName|| +"").toLowerCase()))if(d=a.className.replace(i," ")==b.className.replace(i," "))a:if(a.attributes.length!=b.attributes.length)d=!1;else{d=0;for(var e=a.attributes.length,f,g;d<e;++d)if(f=a.attributes[d],g=f.name,"class"!=g&&(g=b.attributes.getNamedItem(g),f.specified!=g.specified||f.specified&&f.nodeValue!==g.nodeValue)){d=!1;break a}d=!0}return d},createContainer:function(a){a=a.createElement(this.tagNames[0]);this.cssClass&&(a.className=this.cssClass);return a},applyToTextNode:function(a){var b= +a.parentNode;1==b.childNodes.length&&c.dom.arrayContains(this.tagNames,b.tagName.toLowerCase())?this.cssClass&&(a=this.cssClass,b.className?(b.className&&(b.className=b.className.replace(this.similarClassRegExp,"")),b.className+=" "+a):b.className=a):(b=this.createContainer(c.dom.getDocument(a)),a.parentNode.insertBefore(b,a),b.appendChild(a))},isRemovable:function(a){return c.dom.arrayContains(this.tagNames,a.tagName.toLowerCase())&&b.lang.string(a.className).trim()==this.cssClass},undoToTextNode:function(b, +c,e){c.containsNode(e)||(b=c.cloneRange(),b.selectNode(e),b.isPointInRange(c.endContainer,c.endOffset)&&a(c.endContainer,c.endOffset)&&(d(e,c.endContainer,c.endOffset),c.setEndAfter(e)),b.isPointInRange(c.startContainer,c.startOffset)&&a(c.startContainer,c.startOffset)&&(e=d(e,c.startContainer,c.startOffset)));this.similarClassRegExp&&e.className&&(e.className=e.className.replace(this.similarClassRegExp,""));if(this.isRemovable(e)){c=e;for(e=c.parentNode;c.firstChild;)e.insertBefore(c.firstChild, +c);e.removeChild(c)}},applyToRange:function(a){var c=a.getNodes([b.TEXT_NODE]);if(!c.length)try{var d=this.createContainer(a.endContainer.ownerDocument);a.surroundContents(d);this.selectNode(a,d);return}catch(e){}a.splitBoundaries();c=a.getNodes([b.TEXT_NODE]);if(c.length){for(var f=0,g=c.length;f<g;++f)d=c[f],this.getAncestorWithClass(d)||this.applyToTextNode(d);a.setStart(c[0],0);d=c[c.length-1];a.setEnd(d,d.length);this.normalize&&this.postApply(c,a)}},undoToRange:function(a){var c=a.getNodes([b.TEXT_NODE]), +d,e;c.length?(a.splitBoundaries(),c=a.getNodes([b.TEXT_NODE])):(c=a.endContainer.ownerDocument.createTextNode(b.INVISIBLE_SPACE),a.insertNode(c),a.selectNode(c),c=[c]);for(var f=0,g=c.length;f<g;++f)d=c[f],(e=this.getAncestorWithClass(d))&&this.undoToTextNode(d,a,e);1==g?this.selectNode(a,c[0]):(a.setStart(c[0],0),d=c[c.length-1],a.setEnd(d,d.length),this.normalize&&this.postApply(c,a))},selectNode:function(a,c){var d=c.nodeType===b.ELEMENT_NODE,e="canHaveHTML"in c?c.canHaveHTML:!0,f=d?c.innerHTML: +c.data;if((f=""===f||f===b.INVISIBLE_SPACE)&&d&&e)try{c.innerHTML=b.INVISIBLE_SPACE}catch(g){}a.selectNodeContents(c);f&&d?a.collapse(!1):f&&(a.setStartAfter(c),a.setEndAfter(c))},getTextSelectedByRange:function(a,b){var c=b.cloneRange();c.selectNodeContents(a);var d=c.intersection(b),d=d?d.toString():"";c.detach();return d},isAppliedToRange:function(a){var c=[],d,e=a.getNodes([b.TEXT_NODE]);if(!e.length)return(d=this.getAncestorWithClass(a.startContainer))?[d]:!1;for(var f=0,g=e.length,i;f<g;++f){i= +this.getTextSelectedByRange(e[f],a);d=this.getAncestorWithClass(e[f]);if(""!=i&&!d)return!1;c.push(d)}return c},toggleRange:function(a){this.isAppliedToRange(a)?this.undoToRange(a):this.applyToRange(a)}};b.selection.HTMLApplier=f})(wysihtml5,rangy); +wysihtml5.Commands=Base.extend({constructor:function(b){this.editor=b;this.composer=b.composer;this.doc=this.composer.doc},support:function(b){return wysihtml5.browser.supportsCommand(this.doc,b)},exec:function(b,c){var a=wysihtml5.commands[b],d=wysihtml5.lang.array(arguments).get(),e=a&&a.exec,f=null;this.editor.fire("beforecommand:composer");if(e)d.unshift(this.composer),f=e.apply(a,d);else try{f=this.doc.execCommand(b,!1,c)}catch(g){}this.editor.fire("aftercommand:composer");return f},state:function(b, +c){var a=wysihtml5.commands[b],d=wysihtml5.lang.array(arguments).get(),e=a&&a.state;if(e)return d.unshift(this.composer),e.apply(a,d);try{return this.doc.queryCommandState(b)}catch(f){return!1}},value:function(b){var c=wysihtml5.commands[b],a=c&&c.value;if(a)return a.call(c,this.composer,b);try{return this.doc.queryCommandValue(b)}catch(d){return null}}}); +(function(b){b.commands.bold={exec:function(c,a){return b.commands.formatInline.exec(c,a,"b")},state:function(c,a){return b.commands.formatInline.state(c,a,"b")},value:function(){}}})(wysihtml5); +(function(b){function c(c,g){var i=c.doc,h="_wysihtml5-temp-"+ +new Date,k=0,j,n,p;b.commands.formatInline.exec(c,a,d,h,/non-matching-class/g);j=i.querySelectorAll(d+"."+h);for(h=j.length;k<h;k++)for(p in n=j[k],n.removeAttribute("class"),g)n.setAttribute(p,g[p]);k=n;1===h&&(p=e.getTextContent(n),h=!!n.querySelector("*"),p=""===p||p===b.INVISIBLE_SPACE,!h&&p&&(e.setTextContent(n,g.text||n.href),i=i.createTextNode(" "),c.selection.setAfter(n),c.selection.insertNode(i),k=i));c.selection.setAfter(k)} +var a,d="A",e=b.dom;b.commands.createLink={exec:function(a,b,d){var h=this.state(a,b);h?a.selection.executeAndRestore(function(){for(var a=h.length,b=0,c,d,f;b<a;b++)c=h[b],d=e.getParentElement(c,{nodeName:"code"}),f=e.getTextContent(c),f.match(e.autoLink.URL_REG_EXP)&&!d?e.renameElement(c,"code"):e.replaceWithChildNodes(c)}):(d="object"===typeof d?d:{href:d},c(a,d))},state:function(a,c){return b.commands.formatInline.state(a,c,"A")},value:function(){return a}}})(wysihtml5); +(function(b){var c=/wysiwyg-font-size-[a-z\-]+/g;b.commands.fontSize={exec:function(a,d,e){return b.commands.formatInline.exec(a,d,"span","wysiwyg-font-size-"+e,c)},state:function(a,d,e){return b.commands.formatInline.state(a,d,"span","wysiwyg-font-size-"+e,c)},value:function(){}}})(wysihtml5); +(function(b){var c=/wysiwyg-color-[a-z]+/g;b.commands.foreColor={exec:function(a,d,e){return b.commands.formatInline.exec(a,d,"span","wysiwyg-color-"+e,c)},state:function(a,d,e){return b.commands.formatInline.state(a,d,"span","wysiwyg-color-"+e,c)},value:function(){}}})(wysihtml5); +(function(b){function c(a){for(a=a.previousSibling;a&&a.nodeType===b.TEXT_NODE&&!b.lang.string(a.data).trim();)a=a.previousSibling;return a}function a(a){for(a=a.nextSibling;a&&a.nodeType===b.TEXT_NODE&&!b.lang.string(a.data).trim();)a=a.nextSibling;return a}function d(a){return"BR"===a.nodeName||"block"===g.getStyle("display").from(a)?!0:!1}function e(a,c,d,e){if(e)var f=g.observe(a,"DOMNodeInserted",function(a){var a=a.target,c;a.nodeType===b.ELEMENT_NODE&&(c=g.getStyle("display").from(a),"inline"!== +c.substr(0,6)&&(a.className+=" "+e))});a.execCommand(c,!1,d);f&&f.stop()}function f(b,d){b.selection.selectLine();b.selection.surround(d);var e=a(d),f=c(d);e&&"BR"===e.nodeName&&e.parentNode.removeChild(e);f&&"BR"===f.nodeName&&f.parentNode.removeChild(f);(e=d.lastChild)&&"BR"===e.nodeName&&e.parentNode.removeChild(e);b.selection.selectNode(d)}var g=b.dom,i="H1 H2 H3 H4 H5 H6 P BLOCKQUOTE DIV".split(" ");b.commands.formatBlock={exec:function(h,k,j,n,p){var q=h.doc,r=this.state(h,k,j,n,p),m,j="string"=== +typeof j?j.toUpperCase():j;if(r)h.selection.executeAndRestoreSimple(function(){p&&(r.className=r.className.replace(p,""));var e=!!b.lang.string(r.className).trim();if(!e&&r.nodeName===(j||"DIV")){var e=r,f=e.ownerDocument,h=a(e),i=c(e);h&&!d(h)&&e.parentNode.insertBefore(f.createElement("br"),h);i&&!d(i)&&e.parentNode.insertBefore(f.createElement("br"),e);g.replaceWithChildNodes(r)}else e&&g.renameElement(r,"DIV")});else{if(null===j||b.lang.array(i).contains(j))if(m=h.selection.getSelectedNode(), +r=g.getParentElement(m,{nodeName:i})){h.selection.executeAndRestoreSimple(function(){j&&(r=g.renameElement(r,j));if(n){var a=r;a.className?(a.className=a.className.replace(p,""),a.className+=" "+n):a.className=n}});return}h.commands.support(k)?e(q,k,j||"DIV",n):(r=q.createElement(j||"DIV"),n&&(r.className=n),f(h,r))}},state:function(a,b,c,d,e){c="string"===typeof c?c.toUpperCase():c;a=a.selection.getSelectedNode();return g.getParentElement(a,{nodeName:c,className:d,classRegExp:e})},value:function(){}}})(wysihtml5); +(function(b){function c(c,f,g){var i=c+":"+f;if(!d[i]){var h=d,k=b.selection.HTMLApplier,j=a[c],c=j?[c.toLowerCase(),j.toLowerCase()]:[c.toLowerCase()];h[i]=new k(c,f,g,!0)}return d[i]}var a={strong:"b",em:"i",b:"strong",i:"em"},d={};b.commands.formatInline={exec:function(a,b,d,i,h){b=a.selection.getRange();if(!b)return!1;c(d,i,h).toggleRange(b);a.selection.setSelection(b)},state:function(d,f,g,i,h){var f=d.doc,k=a[g]||g;if(!b.dom.hasElementWithTagName(f,g)&&!b.dom.hasElementWithTagName(f,k)||i&& +!b.dom.hasElementWithClassName(f,i))return!1;d=d.selection.getRange();return!d?!1:c(g,i,h).isAppliedToRange(d)},value:function(){}}})(wysihtml5);(function(b){b.commands.insertHTML={exec:function(b,a,d){b.commands.support(a)?b.doc.execCommand(a,!1,d):b.selection.insertHTML(d)},state:function(){return!1},value:function(){}}})(wysihtml5); +(function(b){b.commands.insertImage={exec:function(c,a,d){var d="object"===typeof d?d:{src:d},e=c.doc,a=this.state(c),f;if(a)c.selection.setBefore(a),d=a.parentNode,d.removeChild(a),b.dom.removeEmptyTextNodes(d),"A"===d.nodeName&&!d.firstChild&&(c.selection.setAfter(d),d.parentNode.removeChild(d)),b.quirks.redraw(c.element);else{a=e.createElement("IMG");for(f in d)a[f]=d[f];c.selection.insertNode(a);b.browser.hasProblemsSettingCaretAfterImg()?(d=e.createTextNode(b.INVISIBLE_SPACE),c.selection.insertNode(d), +c.selection.setAfter(d)):c.selection.setAfter(a)}},state:function(c){var a;if(!b.dom.hasElementWithTagName(c.doc,"IMG"))return!1;a=c.selection.getSelectedNode();if(!a)return!1;if("IMG"===a.nodeName)return a;if(a.nodeType!==b.ELEMENT_NODE)return!1;a=c.selection.getText();if(a=b.lang.string(a).trim())return!1;c=c.selection.getNodes(b.ELEMENT_NODE,function(a){return"IMG"===a.nodeName});return 1!==c.length?!1:c[0]},value:function(b){return(b=this.state(b))&&b.src}}})(wysihtml5); +(function(b){var c="<br>"+(b.browser.needsSpaceAfterLineBreak()?" ":"");b.commands.insertLineBreak={exec:function(a,d){a.commands.support(d)?(a.doc.execCommand(d,!1,null),b.browser.autoScrollsToCaret()||a.selection.scrollIntoView()):a.commands.exec("insertHTML",c)},state:function(){return!1},value:function(){}}})(wysihtml5); +(function(b){b.commands.insertOrderedList={exec:function(c,a){var d=c.doc,e=c.selection.getSelectedNode(),f=b.dom.getParentElement(e,{nodeName:"OL"}),g=b.dom.getParentElement(e,{nodeName:"UL"}),e="_wysihtml5-temp-"+(new Date).getTime(),i;c.commands.support(a)?d.execCommand(a,!1,null):f?c.selection.executeAndRestoreSimple(function(){b.dom.resolveList(f)}):g?c.selection.executeAndRestoreSimple(function(){b.dom.renameElement(g,"ol")}):(c.commands.exec("formatBlock","div",e),i=d.querySelector("."+e), +d=""===i.innerHTML||i.innerHTML===b.INVISIBLE_SPACE,c.selection.executeAndRestoreSimple(function(){f=b.dom.convertToList(i,"ol")}),d&&c.selection.selectNode(f.querySelector("li")))},state:function(c){c=c.selection.getSelectedNode();return b.dom.getParentElement(c,{nodeName:"OL"})},value:function(){}}})(wysihtml5); +(function(b){b.commands.insertUnorderedList={exec:function(c,a){var d=c.doc,e=c.selection.getSelectedNode(),f=b.dom.getParentElement(e,{nodeName:"UL"}),g=b.dom.getParentElement(e,{nodeName:"OL"}),e="_wysihtml5-temp-"+(new Date).getTime(),i;c.commands.support(a)?d.execCommand(a,!1,null):f?c.selection.executeAndRestoreSimple(function(){b.dom.resolveList(f)}):g?c.selection.executeAndRestoreSimple(function(){b.dom.renameElement(g,"ul")}):(c.commands.exec("formatBlock","div",e),i=d.querySelector("."+e), +d=""===i.innerHTML||i.innerHTML===b.INVISIBLE_SPACE,c.selection.executeAndRestoreSimple(function(){f=b.dom.convertToList(i,"ul")}),d&&c.selection.selectNode(f.querySelector("li")))},state:function(c){c=c.selection.getSelectedNode();return b.dom.getParentElement(c,{nodeName:"UL"})},value:function(){}}})(wysihtml5);(function(b){b.commands.italic={exec:function(c,a){return b.commands.formatInline.exec(c,a,"i")},state:function(c,a){return b.commands.formatInline.state(c,a,"i")},value:function(){}}})(wysihtml5); +(function(b){var c=/wysiwyg-text-align-[a-z]+/g;b.commands.justifyCenter={exec:function(a){return b.commands.formatBlock.exec(a,"formatBlock",null,"wysiwyg-text-align-center",c)},state:function(a){return b.commands.formatBlock.state(a,"formatBlock",null,"wysiwyg-text-align-center",c)},value:function(){}}})(wysihtml5); +(function(b){var c=/wysiwyg-text-align-[a-z]+/g;b.commands.justifyLeft={exec:function(a){return b.commands.formatBlock.exec(a,"formatBlock",null,"wysiwyg-text-align-left",c)},state:function(a){return b.commands.formatBlock.state(a,"formatBlock",null,"wysiwyg-text-align-left",c)},value:function(){}}})(wysihtml5); +(function(b){var c=/wysiwyg-text-align-[a-z]+/g;b.commands.justifyRight={exec:function(a){return b.commands.formatBlock.exec(a,"formatBlock",null,"wysiwyg-text-align-right",c)},state:function(a){return b.commands.formatBlock.state(a,"formatBlock",null,"wysiwyg-text-align-right",c)},value:function(){}}})(wysihtml5);(function(b){b.commands.underline={exec:function(c,a){return b.commands.formatInline.exec(c,a,"u")},state:function(c,a){return b.commands.formatInline.state(c,a,"u")},value:function(){}}})(wysihtml5); +(function(b){var c='<span id="_wysihtml5-undo" class="_wysihtml5-temp">'+b.INVISIBLE_SPACE+"</span>",a='<span id="_wysihtml5-redo" class="_wysihtml5-temp">'+b.INVISIBLE_SPACE+"</span>",d=b.dom;b.UndoManager=b.lang.Dispatcher.extend({constructor:function(a){this.editor=a;this.composer=a.composer;this.element=this.composer.element;this.history=[this.composer.getValue()];this.position=1;this.composer.commands.support("insertHTML")&&this._observe()},_observe:function(){var e=this,f=this.composer.sandbox.getDocument(), +g;d.observe(this.element,"keydown",function(a){if(!(a.altKey||!a.ctrlKey&&!a.metaKey)){var b=a.keyCode,c=90===b&&a.shiftKey||89===b;90===b&&!a.shiftKey?(e.undo(),a.preventDefault()):c&&(e.redo(),a.preventDefault())}});d.observe(this.element,"keydown",function(a){a=a.keyCode;a!==g&&(g=a,(8===a||46===a)&&e.transact())});if(b.browser.hasUndoInContextMenu()){var i,h,k=function(){for(var a;a=f.querySelector("._wysihtml5-temp");)a.parentNode.removeChild(a);clearInterval(i)};d.observe(this.element,"contextmenu", +function(){k();e.composer.selection.executeAndRestoreSimple(function(){e.element.lastChild&&e.composer.selection.setAfter(e.element.lastChild);f.execCommand("insertHTML",!1,c);f.execCommand("insertHTML",!1,a);f.execCommand("undo",!1,null)});i=setInterval(function(){f.getElementById("_wysihtml5-redo")?(k(),e.redo()):f.getElementById("_wysihtml5-undo")||(k(),e.undo())},400);h||(h=!0,d.observe(document,"mousedown",k),d.observe(f,["mousedown","paste","cut","copy"],k))})}this.editor.observe("newword:composer", +function(){e.transact()}).observe("beforecommand:composer",function(){e.transact()})},transact:function(){var a=this.history[this.position-1],b=this.composer.getValue();if(b!=a){if(40<(this.history.length=this.position))this.history.shift(),this.position--;this.position++;this.history.push(b)}},undo:function(){this.transact();1>=this.position||(this.set(this.history[--this.position-1]),this.editor.fire("undo:composer"))},redo:function(){this.position>=this.history.length||(this.set(this.history[++this.position- +1]),this.editor.fire("redo:composer"))},set:function(a){this.composer.setValue(a);this.editor.focus(!0)}})})(wysihtml5); +wysihtml5.views.View=Base.extend({constructor:function(b,c,a){this.parent=b;this.element=c;this.config=a;this._observeViewChange()},_observeViewChange:function(){var b=this;this.parent.observe("beforeload",function(){b.parent.observe("change_view",function(c){c===b.name?(b.parent.currentView=b,b.show(),setTimeout(function(){b.focus()},0)):b.hide()})})},focus:function(){if(this.element.ownerDocument.querySelector(":focus")!==this.element)try{this.element.focus()}catch(b){}},hide:function(){this.element.style.display= +"none"},show:function(){this.element.style.display=""},disable:function(){this.element.setAttribute("disabled","disabled")},enable:function(){this.element.removeAttribute("disabled")}}); +(function(b){var c=b.dom,a=b.browser;b.views.Composer=b.views.View.extend({name:"composer",CARET_HACK:"<br>",constructor:function(a,b,c){this.base(a,b,c);this.textarea=this.parent.textarea;this._initSandbox()},clear:function(){this.element.innerHTML=a.displaysCaretInEmptyContentEditableCorrectly()?"":this.CARET_HACK},getValue:function(a){var c=this.isEmpty()?"":b.quirks.getCorrectInnerHTML(this.element);a&&(c=this.parent.parse(c));return c=b.lang.string(c).replace(b.INVISIBLE_SPACE).by("")},setValue:function(a, +b){b&&(a=this.parent.parse(a));this.element.innerHTML=a},show:function(){this.iframe.style.display=this._displayStyle||"";this.disable();this.enable()},hide:function(){this._displayStyle=c.getStyle("display").from(this.iframe);"none"===this._displayStyle&&(this._displayStyle=null);this.iframe.style.display="none"},disable:function(){this.element.removeAttribute("contentEditable");this.base()},enable:function(){this.element.setAttribute("contentEditable","true");this.base()},focus:function(a){b.browser.doesAsyncFocus()&& +this.hasPlaceholderSet()&&this.clear();this.base();var c=this.element.lastChild;a&&c&&("BR"===c.nodeName?this.selection.setBefore(this.element.lastChild):this.selection.setAfter(this.element.lastChild))},getTextContent:function(){return c.getTextContent(this.element)},hasPlaceholderSet:function(){return this.getTextContent()==this.textarea.element.getAttribute("placeholder")},isEmpty:function(){var a=this.element.innerHTML;return""===a||a===this.CARET_HACK||this.hasPlaceholderSet()||""===this.getTextContent()&& +!this.element.querySelector("blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea")},_initSandbox:function(){var a=this;this.sandbox=new c.Sandbox(function(){a._create()},{stylesheets:this.config.stylesheets});this.iframe=this.sandbox.getIframe();var b=document.createElement("input");b.type="hidden";b.name="_wysihtml5_mode";b.value=1;var f=this.textarea.element;c.insert(this.iframe).after(f);c.insert(b).after(f)},_create:function(){var d=this;this.doc= +this.sandbox.getDocument();this.element=this.doc.body;this.textarea=this.parent.textarea;this.element.innerHTML=this.textarea.getValue(!0);this.enable();this.selection=new b.Selection(this.parent);this.commands=new b.Commands(this.parent);c.copyAttributes("className spellcheck title lang dir accessKey".split(" ")).from(this.textarea.element).to(this.element);c.addClass(this.element,this.config.composerClassName);this.config.style&&this.style();this.observe();var e=this.config.name;e&&(c.addClass(this.element, +e),c.addClass(this.iframe,e));(e="string"===typeof this.config.placeholder?this.config.placeholder:this.textarea.element.getAttribute("placeholder"))&&c.simulatePlaceholder(this.parent,this,e);this.commands.exec("styleWithCSS",!1);this._initAutoLinking();this._initObjectResizing();this._initUndoManager();(this.textarea.element.hasAttribute("autofocus")||document.querySelector(":focus")==this.textarea.element)&&setTimeout(function(){d.focus()},100);b.quirks.insertLineBreakOnReturn(this);a.clearsContentEditableCorrectly()|| +b.quirks.ensureProperClearing(this);a.clearsListsInContentEditableCorrectly()||b.quirks.ensureProperClearingOfLists(this);this.initSync&&this.config.sync&&this.initSync();this.textarea.hide();this.parent.fire("beforeload").fire("load")},_initAutoLinking:function(){var d=this,e=a.canDisableAutoLinking(),f=a.doesAutoLinkingInContentEditable();e&&this.commands.exec("autoUrlDetect",!1);if(this.config.autoLink){(!f||f&&e)&&this.parent.observe("newword:composer",function(){d.selection.executeAndRestore(function(a, +b){c.autoLink(b.parentNode)})});var g=this.sandbox.getDocument().getElementsByTagName("a"),i=c.autoLink.URL_REG_EXP,h=function(a){a=b.lang.string(c.getTextContent(a)).trim();"www."===a.substr(0,4)&&(a="http://"+a);return a};c.observe(this.element,"keydown",function(a){if(g.length){var a=d.selection.getSelectedNode(a.target.ownerDocument),b=c.getParentElement(a,{nodeName:"A"},4),e;b&&(e=h(b),setTimeout(function(){var a=h(b);a!==e&&a.match(i)&&b.setAttribute("href",a)},0))}})}},_initObjectResizing:function(){var d= +["width","height"],e=d.length,f=this.element;this.commands.exec("enableObjectResizing",this.config.allowObjectResizing);this.config.allowObjectResizing?a.supportsEvent("resizeend")&&c.observe(f,"resizeend",function(a){for(var a=a.target||a.srcElement,c=a.style,h=0,k;h<e;h++)k=d[h],c[k]&&(a.setAttribute(k,parseInt(c[k],10)),c[k]="");b.quirks.redraw(f)}):a.supportsEvent("resizestart")&&c.observe(f,"resizestart",function(a){a.preventDefault()})},_initUndoManager:function(){new b.UndoManager(this.parent)}})})(wysihtml5); +(function(b){var c=b.dom,a=document,d=window,e=a.createElement("div"),f="background-color color cursor font-family font-size font-style font-variant font-weight line-height letter-spacing text-align text-decoration text-indent text-rendering word-break word-wrap word-spacing".split(" "),g="background-color border-collapse border-bottom-color border-bottom-style border-bottom-width border-left-color border-left-style border-left-width border-right-color border-right-style border-right-width border-top-color border-top-style border-top-width clear display float margin-bottom margin-left margin-right margin-top outline-color outline-offset outline-width outline-style padding-left padding-right padding-top padding-bottom position top left right bottom z-index vertical-align text-align -webkit-box-sizing -moz-box-sizing -ms-box-sizing box-sizing -webkit-box-shadow -moz-box-shadow -ms-box-shadow box-shadow -webkit-border-top-right-radius -moz-border-radius-topright border-top-right-radius -webkit-border-bottom-right-radius -moz-border-radius-bottomright border-bottom-right-radius -webkit-border-bottom-left-radius -moz-border-radius-bottomleft border-bottom-left-radius -webkit-border-top-left-radius -moz-border-radius-topleft border-top-left-radius width height".split(" "), +i="width height top left right bottom".split(" "),h=["html { height: 100%; }","body { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }","._wysihtml5-temp { display: none; }",b.browser.isGecko?"body.placeholder { color: graytext !important; }":"body.placeholder { color: #a9a9a9 !important; }","body[disabled] { background-color: #eee !important; color: #999 !important; cursor: default !important; }","img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"], +k=function(b){if(b.setActive)try{b.setActive()}catch(e){}else{var f=b.style,h=a.documentElement.scrollTop||a.body.scrollTop,g=a.documentElement.scrollLeft||a.body.scrollLeft,f={position:f.position,top:f.top,left:f.left,WebkitUserSelect:f.WebkitUserSelect};c.setStyles({position:"absolute",top:"-99999px",left:"-99999px",WebkitUserSelect:"none"}).on(b);b.focus();c.setStyles(f).on(b);d.scrollTo&&d.scrollTo(g,h)}};b.views.Composer.prototype.style=function(){var j=this,n=a.querySelector(":focus"),p=this.textarea.element, +q=p.hasAttribute("placeholder"),r=q&&p.getAttribute("placeholder");this.focusStylesHost=this.focusStylesHost||e.cloneNode(!1);this.blurStylesHost=this.blurStylesHost||e.cloneNode(!1);q&&p.removeAttribute("placeholder");p===n&&p.blur();c.copyStyles(g).from(p).to(this.iframe).andTo(this.blurStylesHost);c.copyStyles(f).from(p).to(this.element).andTo(this.blurStylesHost);c.insertCSS(h).into(this.element.ownerDocument);k(p);c.copyStyles(g).from(p).to(this.focusStylesHost);c.copyStyles(f).from(p).to(this.focusStylesHost); +var m=b.lang.array(g).without(["display"]);n?n.focus():p.blur();q&&p.setAttribute("placeholder",r);if(!b.browser.hasCurrentStyleProperty())var s=c.observe(d,"resize",function(){if(c.contains(document.documentElement,j.iframe)){var a=c.getStyle("display").from(p),b=c.getStyle("display").from(j.iframe);p.style.display="";j.iframe.style.display="none";c.copyStyles(i).from(p).to(j.iframe).andTo(j.focusStylesHost).andTo(j.blurStylesHost);j.iframe.style.display=b;p.style.display=a}else s.stop()});this.parent.observe("focus:composer", +function(){c.copyStyles(m).from(j.focusStylesHost).to(j.iframe);c.copyStyles(f).from(j.focusStylesHost).to(j.element)});this.parent.observe("blur:composer",function(){c.copyStyles(m).from(j.blurStylesHost).to(j.iframe);c.copyStyles(f).from(j.blurStylesHost).to(j.element)});return this}})(wysihtml5); +(function(b){var c=b.dom,a=b.browser,d={66:"bold",73:"italic",85:"underline"};b.views.Composer.prototype.observe=function(){var e=this,f=this.getValue(),g=this.sandbox.getIframe(),i=this.element,h=a.supportsEventsInIframeCorrectly()?i:this.sandbox.getWindow(),k=a.supportsEvent("drop")?["drop","paste"]:["dragdrop","paste"];c.observe(g,"DOMNodeRemoved",function(){clearInterval(j);e.parent.fire("destroy:composer")});var j=setInterval(function(){c.contains(document.documentElement,g)||(clearInterval(j), +e.parent.fire("destroy:composer"))},250);c.observe(h,"focus",function(){e.parent.fire("focus").fire("focus:composer");setTimeout(function(){f=e.getValue()},0)});c.observe(h,"blur",function(){f!==e.getValue()&&e.parent.fire("change").fire("change:composer");e.parent.fire("blur").fire("blur:composer")});b.browser.isIos()&&c.observe(i,"blur",function(){var a=i.ownerDocument.createElement("input"),b=document.documentElement.scrollTop||document.body.scrollTop,c=document.documentElement.scrollLeft||document.body.scrollLeft; +try{e.selection.insertNode(a)}catch(d){i.appendChild(a)}a.focus();a.parentNode.removeChild(a);window.scrollTo(c,b)});c.observe(i,"dragenter",function(){e.parent.fire("unset_placeholder")});a.firesOnDropOnlyWhenOnDragOverIsCancelled()&&c.observe(i,["dragover","dragenter"],function(a){a.preventDefault()});c.observe(i,k,function(b){var c=b.dataTransfer,d;c&&a.supportsDataTransfer()&&(d=c.getData("text/html")||c.getData("text/plain"));d?(i.focus(),e.commands.exec("insertHTML",d),e.parent.fire("paste").fire("paste:composer"), +b.stopPropagation(),b.preventDefault()):setTimeout(function(){e.parent.fire("paste").fire("paste:composer")},0)});c.observe(i,"keyup",function(a){a=a.keyCode;(a===b.SPACE_KEY||a===b.ENTER_KEY)&&e.parent.fire("newword:composer")});this.parent.observe("paste:composer",function(){setTimeout(function(){e.parent.fire("newword:composer")},0)});a.canSelectImagesInContentEditable()||c.observe(i,"mousedown",function(a){var b=a.target;"IMG"===b.nodeName&&(e.selection.selectNode(b),a.preventDefault())});c.observe(i, +"keydown",function(a){var b=d[a.keyCode];if((a.ctrlKey||a.metaKey)&&!a.altKey&&b)e.commands.exec(b),a.preventDefault()});c.observe(i,"keydown",function(a){var c=e.selection.getSelectedNode(!0),d=a.keyCode;if(c&&"IMG"===c.nodeName&&(d===b.BACKSPACE_KEY||d===b.DELETE_KEY))d=c.parentNode,d.removeChild(c),"A"===d.nodeName&&!d.firstChild&&d.parentNode.removeChild(d),setTimeout(function(){b.quirks.redraw(i)},0),a.preventDefault()});var n={IMG:"Image: ",A:"Link: "};c.observe(i,"mouseover",function(a){var a= +a.target,b=a.nodeName;!("A"!==b&&"IMG"!==b)&&!a.hasAttribute("title")&&(b=n[b]+(a.getAttribute("href")||a.getAttribute("src")),a.setAttribute("title",b))})}})(wysihtml5); +(function(b){b.views.Synchronizer=Base.extend({constructor:function(b,a,d){this.editor=b;this.textarea=a;this.composer=d;this._observe()},fromComposerToTextarea:function(c){this.textarea.setValue(b.lang.string(this.composer.getValue()).trim(),c)},fromTextareaToComposer:function(b){var a=this.textarea.getValue();a?this.composer.setValue(a,b):(this.composer.clear(),this.editor.fire("set_placeholder"))},sync:function(b){"textarea"===this.editor.currentView.name?this.fromTextareaToComposer(b):this.fromComposerToTextarea(b)}, +_observe:function(){var c,a=this,d=this.textarea.element.form,e=function(){c=setInterval(function(){a.fromComposerToTextarea()},400)},f=function(){clearInterval(c);c=null};e();d&&(b.dom.observe(d,"submit",function(){a.sync(!0)}),b.dom.observe(d,"reset",function(){setTimeout(function(){a.fromTextareaToComposer()},0)}));this.editor.observe("change_view",function(b){if(b==="composer"&&!c){a.fromTextareaToComposer(true);e()}else if(b==="textarea"){a.fromComposerToTextarea(true);f()}});this.editor.observe("destroy:composer", +f)}})})(wysihtml5); +wysihtml5.views.Textarea=wysihtml5.views.View.extend({name:"textarea",constructor:function(b,c,a){this.base(b,c,a);this._observe()},clear:function(){this.element.value=""},getValue:function(b){var c=this.isEmpty()?"":this.element.value;b&&(c=this.parent.parse(c));return c},setValue:function(b,c){c&&(b=this.parent.parse(b));this.element.value=b},hasPlaceholderSet:function(){var b=wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),c=this.element.getAttribute("placeholder")||null,a=this.element.value; +return b&&!a||a===c},isEmpty:function(){return!wysihtml5.lang.string(this.element.value).trim()||this.hasPlaceholderSet()},_observe:function(){var b=this.element,c=this.parent,a={focusin:"focus",focusout:"blur"},d=wysihtml5.browser.supportsEvent("focusin")?["focusin","focusout","change"]:["focus","blur","change"];c.observe("beforeload",function(){wysihtml5.dom.observe(b,d,function(b){b=a[b.type]||b.type;c.fire(b).fire(b+":textarea")});wysihtml5.dom.observe(b,["paste","drop"],function(){setTimeout(function(){c.fire("paste").fire("paste:textarea")}, +0)})})}}); +(function(b){var c=b.dom;b.toolbar.Dialog=b.lang.Dispatcher.extend({constructor:function(a,b){this.link=a;this.container=b},_observe:function(){if(!this._observed){var a=this,d=function(b){var c=a._serialize();c==a.elementToChange?a.fire("edit",c):a.fire("save",c);a.hide();b.preventDefault();b.stopPropagation()};c.observe(a.link,"click",function(){c.hasClass(a.link,"wysihtml5-command-dialog-opened")&&setTimeout(function(){a.hide()},0)});c.observe(this.container,"keydown",function(c){var e=c.keyCode; +e===b.ENTER_KEY&&d(c);e===b.ESCAPE_KEY&&a.hide()});c.delegate(this.container,"[data-wysihtml5-dialog-action=save]","click",d);c.delegate(this.container,"[data-wysihtml5-dialog-action=cancel]","click",function(b){a.fire("cancel");a.hide();b.preventDefault();b.stopPropagation()});for(var e=this.container.querySelectorAll("input, select, textarea"),f=0,g=e.length,i=function(){clearInterval(a.interval)};f<g;f++)c.observe(e[f],"change",i);this._observed=!0}},_serialize:function(){for(var a=this.elementToChange|| +{},b=this.container.querySelectorAll("[data-wysihtml5-dialog-field]"),c=b.length,f=0;f<c;f++)a[b[f].getAttribute("data-wysihtml5-dialog-field")]=b[f].value;return a},_interpolate:function(a){for(var b,c,f=document.querySelector(":focus"),g=this.container.querySelectorAll("[data-wysihtml5-dialog-field]"),i=g.length,h=0;h<i;h++)b=g[h],b!==f&&!(a&&"hidden"===b.type)&&(c=b.getAttribute("data-wysihtml5-dialog-field"),c=this.elementToChange?this.elementToChange[c]||"":b.defaultValue,b.value=c)},show:function(a){var b= +this,e=this.container.querySelector("input, select, textarea");this.elementToChange=a;this._observe();this._interpolate();a&&(this.interval=setInterval(function(){b._interpolate(!0)},500));c.addClass(this.link,"wysihtml5-command-dialog-opened");this.container.style.display="";this.fire("show");if(e&&!a)try{e.focus()}catch(f){}},hide:function(){clearInterval(this.interval);this.elementToChange=null;c.removeClass(this.link,"wysihtml5-command-dialog-opened");this.container.style.display="none";this.fire("hide")}})})(wysihtml5); +(function(b){var c=b.dom,a={position:"relative"},d={left:0,margin:0,opacity:0,overflow:"hidden",padding:0,position:"absolute",top:0,zIndex:1},e={cursor:"inherit",fontSize:"50px",height:"50px",marginTop:"-25px",outline:0,padding:0,position:"absolute",right:"-4px",top:"50%"},f={"x-webkit-speech":"",speech:""};b.toolbar.Speech=function(g,i){var h=document.createElement("input");if(b.browser.supportsSpeechApiOn(h)){var k=document.createElement("div");b.lang.object(d).merge({width:i.offsetWidth+"px",height:i.offsetHeight+ +"px"});c.insert(h).into(k);c.insert(k).into(i);c.setStyles(e).on(h);c.setAttributes(f).on(h);c.setStyles(d).on(k);c.setStyles(a).on(i);c.observe(h,"onwebkitspeechchange"in h?"webkitspeechchange":"speechchange",function(){g.execCommand("insertText",h.value);h.value=""});c.observe(h,"click",function(a){c.hasClass(i,"wysihtml5-command-disabled")&&a.preventDefault();a.stopPropagation()})}else i.style.display="none"}})(wysihtml5); +(function(b){var c=b.dom;b.toolbar.Toolbar=Base.extend({constructor:function(a,c){this.editor=a;this.container="string"===typeof c?document.getElementById(c):c;this.composer=a.composer;this._getLinks("command");this._getLinks("action");this._observe();this.show();for(var e=this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),f=e.length,g=0;g<f;g++)new b.toolbar.Speech(this,e[g])},_getLinks:function(a){for(var c=this[a+"Links"]=b.lang.array(this.container.querySelectorAll("[data-wysihtml5-"+ +a+"]")).get(),e=c.length,f=0,g=this[a+"Mapping"]={},i,h,k,j,n;f<e;f++)i=c[f],k=i.getAttribute("data-wysihtml5-"+a),j=i.getAttribute("data-wysihtml5-"+a+"-value"),h=this.container.querySelector("[data-wysihtml5-"+a+"-group='"+k+"']"),n=this._getDialog(i,k),g[k+":"+j]={link:i,group:h,name:k,value:j,dialog:n,state:!1}},_getDialog:function(a,c){var e=this,f=this.container.querySelector("[data-wysihtml5-dialog='"+c+"']"),g,i;f&&(g=new b.toolbar.Dialog(a,f),g.observe("show",function(){i=e.composer.selection.getBookmark(); +e.editor.fire("show:dialog",{command:c,dialogContainer:f,commandLink:a})}),g.observe("save",function(b){i&&e.composer.selection.setBookmark(i);e._execCommand(c,b);e.editor.fire("save:dialog",{command:c,dialogContainer:f,commandLink:a})}),g.observe("cancel",function(){e.editor.focus(!1);e.editor.fire("cancel:dialog",{command:c,dialogContainer:f,commandLink:a})}));return g},execCommand:function(a,b){if(!this.commandsDisabled){var c=this.commandMapping[a+":"+b];c&&c.dialog&&!c.state?c.dialog.show(): +this._execCommand(a,b)}},_execCommand:function(a,b){this.editor.focus(!1);this.composer.commands.exec(a,b);this._updateLinkStates()},execAction:function(a){var b=this.editor;switch(a){case "change_view":b.currentView===b.textarea?b.fire("change_view","composer"):b.fire("change_view","textarea")}},_observe:function(){for(var a=this,b=this.editor,e=this.container,f=this.commandLinks.concat(this.actionLinks),g=f.length,i=0;i<g;i++)c.setAttributes({href:"javascript:;",unselectable:"on"}).on(f[i]);c.delegate(e, +"[data-wysihtml5-command]","mousedown",function(a){a.preventDefault()});c.delegate(e,"[data-wysihtml5-command]","click",function(b){var c=this.getAttribute("data-wysihtml5-command"),d=this.getAttribute("data-wysihtml5-command-value");a.execCommand(c,d);b.preventDefault()});c.delegate(e,"[data-wysihtml5-action]","click",function(b){var c=this.getAttribute("data-wysihtml5-action");a.execAction(c);b.preventDefault()});b.observe("focus:composer",function(){a.bookmark=null;clearInterval(a.interval);a.interval= +setInterval(function(){a._updateLinkStates()},500)});b.observe("blur:composer",function(){clearInterval(a.interval)});b.observe("destroy:composer",function(){clearInterval(a.interval)});b.observe("change_view",function(b){setTimeout(function(){a.commandsDisabled="composer"!==b;a._updateLinkStates();a.commandsDisabled?c.addClass(e,"wysihtml5-commands-disabled"):c.removeClass(e,"wysihtml5-commands-disabled")},0)})},_updateLinkStates:function(){var a=this.commandMapping,d=this.actionMapping,e,f,g;for(e in a)if(g= +a[e],this.commandsDisabled?(f=!1,c.removeClass(g.link,"wysihtml5-command-active"),g.group&&c.removeClass(g.group,"wysihtml5-command-active"),g.dialog&&g.dialog.hide()):(f=this.composer.commands.state(g.name,g.value),b.lang.object(f).isArray()&&(f=1===f.length?f[0]:!0),c.removeClass(g.link,"wysihtml5-command-disabled"),g.group&&c.removeClass(g.group,"wysihtml5-command-disabled")),g.state!==f)(g.state=f)?(c.addClass(g.link,"wysihtml5-command-active"),g.group&&c.addClass(g.group,"wysihtml5-command-active"), +g.dialog&&("object"===typeof f?g.dialog.show(f):g.dialog.hide())):(c.removeClass(g.link,"wysihtml5-command-active"),g.group&&c.removeClass(g.group,"wysihtml5-command-active"),g.dialog&&g.dialog.hide());for(e in d)a=d[e],"change_view"===a.name&&(a.state=this.editor.currentView===this.editor.textarea,a.state?c.addClass(a.link,"wysihtml5-action-active"):c.removeClass(a.link,"wysihtml5-action-active"))},show:function(){this.container.style.display=""},hide:function(){this.container.style.display="none"}})})(wysihtml5); +(function(b){var c={name:void 0,style:!0,toolbar:void 0,autoLink:!0,parserRules:{tags:{br:{},span:{},div:{},p:{}},classes:{}},parser:b.dom.parse,composerClassName:"wysihtml5-editor",bodyClassName:"wysihtml5-supported",stylesheets:[],placeholderText:void 0,allowObjectResizing:!0,supportTouchDevices:!0};b.Editor=b.lang.Dispatcher.extend({constructor:function(a,d){this.textareaElement="string"===typeof a?document.getElementById(a):a;this.config=b.lang.object({}).merge(c).merge(d).get();this.currentView= +this.textarea=new b.views.Textarea(this,this.textareaElement,this.config);this._isCompatible=b.browser.supported();if(!this._isCompatible||!this.config.supportTouchDevices&&b.browser.isTouchDevice()){var e=this;setTimeout(function(){e.fire("beforeload").fire("load")},0)}else{b.dom.addClass(document.body,this.config.bodyClassName);this.currentView=this.composer=new b.views.Composer(this,this.textareaElement,this.config);"function"===typeof this.config.parser&&this._initParser();this.observe("beforeload", +function(){this.synchronizer=new b.views.Synchronizer(this,this.textarea,this.composer);this.config.toolbar&&(this.toolbar=new b.toolbar.Toolbar(this,this.config.toolbar))});try{console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5")}catch(f){}}},isCompatible:function(){return this._isCompatible},clear:function(){this.currentView.clear();return this},getValue:function(a){return this.currentView.getValue(a)},setValue:function(a,b){if(!a)return this.clear(); +this.currentView.setValue(a,b);return this},focus:function(a){this.currentView.focus(a);return this},disable:function(){this.currentView.disable();return this},enable:function(){this.currentView.enable();return this},isEmpty:function(){return this.currentView.isEmpty()},hasPlaceholderSet:function(){return this.currentView.hasPlaceholderSet()},parse:function(a){var c=this.config.parser(a,this.config.parserRules,this.composer.sandbox.getDocument(),!0);"object"===typeof a&&b.quirks.redraw(a);return c}, +_initParser:function(){this.observe("paste:composer",function(){var a=this;a.composer.selection.executeAndRestore(function(){b.quirks.cleanPastedHTML(a.composer.element);a.parse(a.composer.element)},!0)});this.observe("paste:textarea",function(){this.textarea.setValue(this.parse(this.textarea.getValue()))})}})})(wysihtml5); diff --git a/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysiwyg-color.css b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysiwyg-color.css new file mode 100644 index 0000000..86e7895 --- /dev/null +++ b/dist/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysiwyg-color.css @@ -0,0 +1,67 @@ +.wysiwyg-color-black { + color: black; +} + +.wysiwyg-color-silver { + color: silver; +} + +.wysiwyg-color-gray { + color: gray; +} + +.wysiwyg-color-white { + color: white; +} + +.wysiwyg-color-maroon { + color: maroon; +} + +.wysiwyg-color-red { + color: red; +} + +.wysiwyg-color-purple { + color: purple; +} + +.wysiwyg-color-fuchsia { + color: fuchsia; +} + +.wysiwyg-color-green { + color: green; +} + +.wysiwyg-color-lime { + color: lime; +} + +.wysiwyg-color-olive { + color: olive; +} + +.wysiwyg-color-yellow { + color: yellow; +} + +.wysiwyg-color-navy { + color: navy; +} + +.wysiwyg-color-blue { + color: blue; +} + +.wysiwyg-color-teal { + color: teal; +} + +.wysiwyg-color-aqua { + color: aqua; +} + +.wysiwyg-color-orange { + color: orange; +} \ No newline at end of file diff --git a/dist/inputs-ext/wysihtml5/wysihtml5.js b/dist/inputs-ext/wysihtml5/wysihtml5.js new file mode 100644 index 0000000..12dc33c --- /dev/null +++ b/dist/inputs-ext/wysihtml5/wysihtml5.js @@ -0,0 +1,124 @@ +/** +Bootstrap wysihtml5 editor. Based on [bootstrap-wysihtml5](https://github.com/jhollingworth/bootstrap-wysihtml5). +You should include this input **manually** with dependent js and css files from `inputs-ext` directory. + + <link href="js/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.css" rel="stylesheet" type="text/css"></link> + <script src="js/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.min.js"></script> + <script src="js/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.min.js"></script> + <script src="js/inputs-ext/wysihtml5/wysihtml5.js"></script> + +**Note:** It's better to use fresh bootstrap-wysihtml5 from it's [master branch](https://github.com/jhollingworth/bootstrap-wysihtml5/tree/master/src) as there is update for correct image insertion. + +@class wysihtml5 +@extends abstractinput +@final +@since 1.4.0 +@example +<div id="comments" data-type="wysihtml5" data-pk="1"><h2>awesome</h2> comment!</div> +<script> +$(function(){ + $('#comments').editable({ + url: '/post', + title: 'Enter comments' + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Wysihtml5 = function (options) { + this.init('wysihtml5', options, Wysihtml5.defaults); + + //extend wysihtml5 manually as $.extend not recursive + this.options.wysihtml5 = $.extend({}, Wysihtml5.defaults.wysihtml5, options.wysihtml5); + }; + + $.fn.editableutils.inherit(Wysihtml5, $.fn.editabletypes.abstractinput); + + $.extend(Wysihtml5.prototype, { + render: function () { + var deferred = $.Deferred(), + msieOld; + + //generate unique id as it required for wysihtml5 + this.$input.attr('id', 'textarea_'+(new Date()).getTime()); + + this.setClass(); + this.setAttr('placeholder'); + + //resolve deffered when widget loaded + $.extend(this.options.wysihtml5, { + events: { + load: function() { + deferred.resolve(); + } + } + }); + + this.$input.wysihtml5(this.options.wysihtml5); + + /* + In IE8 wysihtml5 iframe stays on the same line with buttons toolbar (inside popover). + The only solution I found is to add <br>. If you fine better way, please send PR. + */ + msieOld = /msie\s*(8|7|6)/.test(navigator.userAgent.toLowerCase()); + if(msieOld) { + this.$input.before('<br><br>'); + } + + return deferred.promise(); + }, + + value2html: function(value, element) { + $(element).html(value); + }, + + html2value: function(html) { + return html; + }, + + value2input: function(value) { + this.$input.data("wysihtml5").editor.setValue(value, true); + }, + + activate: function() { + this.$input.data("wysihtml5").editor.focus(); + } + }); + + Wysihtml5.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <textarea></textarea> + **/ + tpl:'<textarea></textarea>', + /** + @property inputclass + @default editable-wysihtml5 + **/ + inputclass: 'editable-wysihtml5', + /** + Placeholder attribute of input. Shown when input is empty. + + @property placeholder + @type string + @default null + **/ + placeholder: null, + /** + Wysihtml5 default options. + See https://github.com/jhollingworth/bootstrap-wysihtml5#options + + @property wysihtml5 + @type object + @default {stylesheets: false} + **/ + wysihtml5: { + stylesheets: false //see https://github.com/jhollingworth/bootstrap-wysihtml5/issues/183 + } + }); + + $.fn.editabletypes.wysihtml5 = Wysihtml5; + +}(window.jQuery)); diff --git a/dist/jquery-editable/css/jquery-editable.css b/dist/jquery-editable/css/jquery-editable.css new file mode 100644 index 0000000..b0863f8 --- /dev/null +++ b/dist/jquery-editable/css/jquery-editable.css @@ -0,0 +1,193 @@ +/*! X-editable - v1.4.4 +* 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 */ +} + +.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-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 */ +} +.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: '*'*/ +} + +/*see https://github.com/vitalets/x-editable/issues/139 */ +.form-horizontal .editable +{ + padding-top: 5px; + display:inline-block; +} + diff --git a/dist/jquery-editable/img/clear.png b/dist/jquery-editable/img/clear.png new file mode 100644 index 0000000..580b52a Binary files /dev/null and b/dist/jquery-editable/img/clear.png differ diff --git a/dist/jquery-editable/img/loading.gif b/dist/jquery-editable/img/loading.gif new file mode 100644 index 0000000..5b33f7e Binary files /dev/null and b/dist/jquery-editable/img/loading.gif differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/animated-overlay.gif b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/animated-overlay.gif new file mode 100644 index 0000000..d441f75 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/animated-overlay.gif differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100644 index 0000000..41c8f14 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_flat_55_fbec88_40x100.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_flat_55_fbec88_40x100.png new file mode 100644 index 0000000..1b94a02 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_flat_55_fbec88_40x100.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png new file mode 100644 index 0000000..0345928 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_85_dfeffc_1x400.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_85_dfeffc_1x400.png new file mode 100644 index 0000000..637a592 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_85_dfeffc_1x400.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_95_fef1ec_1x400.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_95_fef1ec_1x400.png new file mode 100644 index 0000000..9f4b7f5 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_glass_95_fef1ec_1x400.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png new file mode 100644 index 0000000..f94cfc0 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_inset-hard_100_f5f8f9_1x100.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_inset-hard_100_f5f8f9_1x100.png new file mode 100644 index 0000000..56c4555 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_inset-hard_100_f5f8f9_1x100.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png new file mode 100644 index 0000000..c428158 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_217bc0_256x240.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_217bc0_256x240.png new file mode 100644 index 0000000..8d2b7e5 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_217bc0_256x240.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_2e83ff_256x240.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_2e83ff_256x240.png new file mode 100644 index 0000000..84b601b Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_2e83ff_256x240.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_469bdd_256x240.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_469bdd_256x240.png new file mode 100644 index 0000000..5dff3f9 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_469bdd_256x240.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_6da8d5_256x240.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_6da8d5_256x240.png new file mode 100644 index 0000000..f7809f8 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_6da8d5_256x240.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_cd0a0a_256x240.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_cd0a0a_256x240.png new file mode 100644 index 0000000..ed5b6b0 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_cd0a0a_256x240.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_d8e7f3_256x240.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_d8e7f3_256x240.png new file mode 100644 index 0000000..9b46228 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_d8e7f3_256x240.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_f9bd01_256x240.png b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_f9bd01_256x240.png new file mode 100644 index 0000000..f1f0531 Binary files /dev/null and b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/images/ui-icons_f9bd01_256x240.png differ diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/jquery-ui-1.10.2.custom.css b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/jquery-ui-1.10.2.custom.css new file mode 100644 index 0000000..f6f72bd --- /dev/null +++ b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/jquery-ui-1.10.2.custom.css @@ -0,0 +1,649 @@ +/*! jQuery UI - v1.10.2 - 2013-04-07 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.datepicker.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Lucida%20Grande%2CLucida%20Sans%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=5px&bgColorHeader=5c9ccc&bgTextureHeader=gloss_wave&bgImgOpacityHeader=55&borderColorHeader=4297d7&fcHeader=ffffff&iconColorHeader=d8e7f3&bgColorContent=fcfdfd&bgTextureContent=inset_hard&bgImgOpacityContent=100&borderColorContent=a6c9e2&fcContent=222222&iconColorContent=469bdd&bgColorDefault=dfeffc&bgTextureDefault=glass&bgImgOpacityDefault=85&borderColorDefault=c5dbec&fcDefault=2e6e9e&iconColorDefault=6da8d5&bgColorHover=d0e5f5&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=79b7e7&fcHover=1d5987&iconColorHover=217bc0&bgColorActive=f5f8f9&bgTextureActive=inset_hard&bgImgOpacityActive=100&borderColorActive=79b7e7&fcActive=e17009&iconColorActive=f9bd01&bgColorHighlight=fbec88&bgTextureHighlight=flat&bgImgOpacityHighlight=55&borderColorHighlight=fad42e&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-clearfix { + min-height: 0; /* support: IE7 */ +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; +} + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month-year { + width: 100%; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 49%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Lucida Grande,Lucida Sans,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Lucida Grande,Lucida Sans,Arial,sans-serif; + font-size: 1em; +} +.ui-widget-content { + border: 1px solid #a6c9e2; + background: #fcfdfd url(images/ui-bg_inset-hard_100_fcfdfd_1x100.png) 50% bottom repeat-x; + color: #222222; +} +.ui-widget-content a { + color: #222222; +} +.ui-widget-header { + border: 1px solid #4297d7; + background: #5c9ccc url(images/ui-bg_gloss-wave_55_5c9ccc_500x100.png) 50% 50% repeat-x; + color: #ffffff; + font-weight: bold; +} +.ui-widget-header a { + color: #ffffff; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { + border: 1px solid #c5dbec; + background: #dfeffc url(images/ui-bg_glass_85_dfeffc_1x400.png) 50% 50% repeat-x; + font-weight: bold; + color: #2e6e9e; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #2e6e9e; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + border: 1px solid #79b7e7; + background: #d0e5f5 url(images/ui-bg_glass_75_d0e5f5_1x400.png) 50% 50% repeat-x; + font-weight: bold; + color: #1d5987; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited { + color: #1d5987; + text-decoration: none; +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #79b7e7; + background: #f5f8f9 url(images/ui-bg_inset-hard_100_f5f8f9_1x100.png) 50% 50% repeat-x; + font-weight: bold; + color: #e17009; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #e17009; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #fad42e; + background: #fbec88 url(images/ui-bg_flat_55_fbec88_40x100.png) 50% 50% repeat-x; + color: #363636; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #cd0a0a; + background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; + color: #cd0a0a; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #cd0a0a; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #cd0a0a; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* For IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url(images/ui-icons_469bdd_256x240.png); +} +.ui-widget-header .ui-icon { + background-image: url(images/ui-icons_d8e7f3_256x240.png); +} +.ui-state-default .ui-icon { + background-image: url(images/ui-icons_6da8d5_256x240.png); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: url(images/ui-icons_217bc0_256x240.png); +} +.ui-state-active .ui-icon { + background-image: url(images/ui-icons_f9bd01_256x240.png); +} +.ui-state-highlight .ui-icon { + background-image: url(images/ui-icons_2e83ff_256x240.png); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url(images/ui-icons_cd0a0a_256x240.png); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 5px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 5px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 5px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 5px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; + opacity: .3; + filter: Alpha(Opacity=30); +} +.ui-widget-shadow { + margin: -8px 0 0 -8px; + padding: 8px; + background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; + opacity: .3; + filter: Alpha(Opacity=30); + border-radius: 8px; +} diff --git a/dist/jquery-editable/jquery-ui-datepicker/css/redmond/jquery-ui-1.10.2.custom.min.css b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/jquery-ui-1.10.2.custom.min.css new file mode 100644 index 0000000..349de88 --- /dev/null +++ b/dist/jquery-editable/jquery-ui-datepicker/css/redmond/jquery-ui-1.10.2.custom.min.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.10.2 - 2013-04-07 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.datepicker.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Lucida%20Grande%2CLucida%20Sans%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=5px&bgColorHeader=5c9ccc&bgTextureHeader=gloss_wave&bgImgOpacityHeader=55&borderColorHeader=4297d7&fcHeader=ffffff&iconColorHeader=d8e7f3&bgColorContent=fcfdfd&bgTextureContent=inset_hard&bgImgOpacityContent=100&borderColorContent=a6c9e2&fcContent=222222&iconColorContent=469bdd&bgColorDefault=dfeffc&bgTextureDefault=glass&bgImgOpacityDefault=85&borderColorDefault=c5dbec&fcDefault=2e6e9e&iconColorDefault=6da8d5&bgColorHover=d0e5f5&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=79b7e7&fcHover=1d5987&iconColorHover=217bc0&bgColorActive=f5f8f9&bgTextureActive=inset_hard&bgImgOpacityActive=100&borderColorActive=79b7e7&fcActive=e17009&iconColorActive=f9bd01&bgColorHighlight=fbec88&bgTextureHighlight=flat&bgImgOpacityHighlight=55&borderColorHighlight=fad42e&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month-year{width:100%}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:700;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-widget{font-family:Lucida Grande,Lucida Sans,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Lucida Grande,Lucida Sans,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #a6c9e2;background:#fcfdfd url(images/ui-bg_inset-hard_100_fcfdfd_1x100.png) 50% bottom repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #4297d7;background:#5c9ccc url(images/ui-bg_gloss-wave_55_5c9ccc_500x100.png) 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #c5dbec;background:#dfeffc url(images/ui-bg_glass_85_dfeffc_1x400.png) 50% 50% repeat-x;font-weight:bold;color:#2e6e9e}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#2e6e9e;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #79b7e7;background:#d0e5f5 url(images/ui-bg_glass_75_d0e5f5_1x400.png) 50% 50% repeat-x;font-weight:bold;color:#1d5987}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited{color:#1d5987;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #79b7e7;background:#f5f8f9 url(images/ui-bg_inset-hard_100_f5f8f9_1x100.png) 50% 50% repeat-x;font-weight:bold;color:#e17009}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#e17009;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fad42e;background:#fbec88 url(images/ui-bg_flat_55_fbec88_40x100.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_469bdd_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_d8e7f3_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_6da8d5_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_217bc0_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_f9bd01_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:5px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:5px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:5px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:5px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} \ No newline at end of file diff --git a/dist/jquery-editable/jquery-ui-datepicker/js/jquery-ui-1.10.2.custom.js b/dist/jquery-editable/jquery-ui-datepicker/js/jquery-ui-1.10.2.custom.js new file mode 100644 index 0000000..4850e0f --- /dev/null +++ b/dist/jquery-editable/jquery-ui-datepicker/js/jquery-ui-1.10.2.custom.js @@ -0,0 +1,2352 @@ +/*! jQuery UI - v1.10.2 - 2013-04-07 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.datepicker.js +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ + +(function( $, undefined ) { + +var uuid = 0, + runiqueId = /^ui-id-\d+$/; + +// $.ui might exist from components with no dependencies, e.g., $.ui.position +$.ui = $.ui || {}; + +$.extend( $.ui, { + version: "1.10.2", + + keyCode: { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + NUMPAD_ADD: 107, + NUMPAD_DECIMAL: 110, + NUMPAD_DIVIDE: 111, + NUMPAD_ENTER: 108, + NUMPAD_MULTIPLY: 106, + NUMPAD_SUBTRACT: 109, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 + } +}); + +// plugins +$.fn.extend({ + focus: (function( orig ) { + return function( delay, fn ) { + return typeof delay === "number" ? + this.each(function() { + var elem = this; + setTimeout(function() { + $( elem ).focus(); + if ( fn ) { + fn.call( elem ); + } + }, delay ); + }) : + orig.apply( this, arguments ); + }; + })( $.fn.focus ), + + scrollParent: function() { + var scrollParent; + if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { + scrollParent = this.parents().filter(function() { + return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); + }).eq(0); + } else { + scrollParent = this.parents().filter(function() { + return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); + }).eq(0); + } + + return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; + }, + + zIndex: function( zIndex ) { + if ( zIndex !== undefined ) { + return this.css( "zIndex", zIndex ); + } + + if ( this.length ) { + var elem = $( this[ 0 ] ), position, value; + while ( elem.length && elem[ 0 ] !== document ) { + // Ignore z-index if position is set to a value where z-index is ignored by the browser + // This makes behavior of this function consistent across browsers + // WebKit always returns auto if the element is positioned + position = elem.css( "position" ); + if ( position === "absolute" || position === "relative" || position === "fixed" ) { + // IE returns 0 when zIndex is not specified + // other browsers return a string + // we ignore the case of nested elements with an explicit value of 0 + // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> + value = parseInt( elem.css( "zIndex" ), 10 ); + if ( !isNaN( value ) && value !== 0 ) { + return value; + } + } + elem = elem.parent(); + } + } + + return 0; + }, + + uniqueId: function() { + return this.each(function() { + if ( !this.id ) { + this.id = "ui-id-" + (++uuid); + } + }); + }, + + removeUniqueId: function() { + return this.each(function() { + if ( runiqueId.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + }); + } +}); + +// selectors +function focusable( element, isTabIndexNotNaN ) { + var map, mapName, img, + nodeName = element.nodeName.toLowerCase(); + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap=#" + mapName + "]" )[0]; + return !!img && visible( img ); + } + return ( /input|select|textarea|button|object/.test( nodeName ) ? + !element.disabled : + "a" === nodeName ? + element.href || isTabIndexNotNaN : + isTabIndexNotNaN) && + // the element and all of its ancestors must be visible + visible( element ); +} + +function visible( element ) { + return $.expr.filters.visible( element ) && + !$( element ).parents().addBack().filter(function() { + return $.css( this, "visibility" ) === "hidden"; + }).length; +} + +$.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo(function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + }) : + // support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + }, + + focusable: function( element ) { + return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); + }, + + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + isTabIndexNaN = isNaN( tabIndex ); + return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); + } +}); + +// support: jQuery <1.8 +if ( !$( "<a>" ).outerWidth( 1 ).jquery ) { + $.each( [ "Width", "Height" ], function( i, name ) { + var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], + type = name.toLowerCase(), + orig = { + innerWidth: $.fn.innerWidth, + innerHeight: $.fn.innerHeight, + outerWidth: $.fn.outerWidth, + outerHeight: $.fn.outerHeight + }; + + function reduce( elem, size, border, margin ) { + $.each( side, function() { + size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; + if ( border ) { + size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; + } + if ( margin ) { + size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; + } + }); + return size; + } + + $.fn[ "inner" + name ] = function( size ) { + if ( size === undefined ) { + return orig[ "inner" + name ].call( this ); + } + + return this.each(function() { + $( this ).css( type, reduce( this, size ) + "px" ); + }); + }; + + $.fn[ "outer" + name] = function( size, margin ) { + if ( typeof size !== "number" ) { + return orig[ "outer" + name ].call( this, size ); + } + + return this.each(function() { + $( this).css( type, reduce( this, size, true, margin ) + "px" ); + }); + }; + }); +} + +// support: jQuery <1.8 +if ( !$.fn.addBack ) { + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} + +// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) +if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { + $.fn.removeData = (function( removeData ) { + return function( key ) { + if ( arguments.length ) { + return removeData.call( this, $.camelCase( key ) ); + } else { + return removeData.call( this ); + } + }; + })( $.fn.removeData ); +} + + + + + +// deprecated +$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); + +$.support.selectstart = "onselectstart" in document.createElement( "div" ); +$.fn.extend({ + disableSelection: function() { + return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + + ".ui-disableSelection", function( event ) { + event.preventDefault(); + }); + }, + + enableSelection: function() { + return this.unbind( ".ui-disableSelection" ); + } +}); + +$.extend( $.ui, { + // $.ui.plugin is deprecated. Use the proxy pattern instead. + plugin: { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); + } + }, + call: function( instance, name, args ) { + var i, + set = instance.plugins[ name ]; + if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { + return; + } + + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } + } + } + }, + + // only used by resizable + hasScroll: function( el, a ) { + + //If overflow is hidden, the element might have extra content, but the user wants to hide it + if ( $( el ).css( "overflow" ) === "hidden") { + return false; + } + + var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", + has = false; + + if ( el[ scroll ] > 0 ) { + return true; + } + + // TODO: determine which cases actually cause this to happen + // if the element doesn't have the scroll set, see if it's possible to + // set the scroll + el[ scroll ] = 1; + has = ( el[ scroll ] > 0 ); + el[ scroll ] = 0; + return has; + } +}); + +})( jQuery ); +(function( $, undefined ) { + +$.extend($.ui, { datepicker: { version: "1.10.2" } }); + +var PROP_NAME = "datepicker", + dpuuid = new Date().getTime(), + instActive; + +/* Date picker manager. + Use the singleton instance of this class, $.datepicker, to interact with the date picker. + Settings for (groups of) date pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ + +function Datepicker() { + this._curInst = null; // The current instance in use + this._keyEvent = false; // If the last event was a key event + this._disabledInputs = []; // List of date picker inputs that have been disabled + this._datepickerShowing = false; // True if the popup picker is showing , false if not + this._inDialog = false; // True if showing within a "dialog", false if not + this._mainDivId = "ui-datepicker-div"; // The ID of the main datepicker division + this._inlineClass = "ui-datepicker-inline"; // The name of the inline marker class + this._appendClass = "ui-datepicker-append"; // The name of the append marker class + this._triggerClass = "ui-datepicker-trigger"; // The name of the trigger marker class + this._dialogClass = "ui-datepicker-dialog"; // The name of the dialog marker class + this._disableClass = "ui-datepicker-disabled"; // The name of the disabled covering marker class + this._unselectableClass = "ui-datepicker-unselectable"; // The name of the unselectable cell marker class + this._currentClass = "ui-datepicker-current-day"; // The name of the current day marker class + this._dayOverClass = "ui-datepicker-days-cell-over"; // The name of the day hover marker class + this.regional = []; // Available regional settings, indexed by language code + this.regional[""] = { // Default regional settings + closeText: "Done", // Display text for close link + prevText: "Prev", // Display text for previous month link + nextText: "Next", // Display text for next month link + currentText: "Today", // Display text for current month link + monthNames: ["January","February","March","April","May","June", + "July","August","September","October","November","December"], // Names of months for drop-down and formatting + monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], // For formatting + dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], // For formatting + dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], // For formatting + dayNamesMin: ["Su","Mo","Tu","We","Th","Fr","Sa"], // Column headings for days starting at Sunday + weekHeader: "Wk", // Column header for week of the year + dateFormat: "mm/dd/yy", // See format options on parseDate + firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ... + isRTL: false, // True if right-to-left language, false if left-to-right + showMonthAfterYear: false, // True if the year select precedes month, false for month then year + yearSuffix: "" // Additional text to append to the year in the month headers + }; + this._defaults = { // Global defaults for all the date picker instances + showOn: "focus", // "focus" for popup on focus, + // "button" for trigger button, or "both" for either + showAnim: "fadeIn", // Name of jQuery animation for popup + showOptions: {}, // Options for enhanced animations + defaultDate: null, // Used when field is blank: actual date, + // +/-number for offset from today, null for today + appendText: "", // Display text following the input box, e.g. showing the format + buttonText: "...", // Text for trigger button + buttonImage: "", // URL for trigger button image + buttonImageOnly: false, // True if the image appears alone, false if it appears on a button + hideIfNoPrevNext: false, // True to hide next/previous month links + // if not applicable, false to just disable them + navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links + gotoCurrent: false, // True if today link goes back to current selection instead + changeMonth: false, // True if month can be selected directly, false if only prev/next + changeYear: false, // True if year can be selected directly, false if only prev/next + yearRange: "c-10:c+10", // Range of years to display in drop-down, + // either relative to today's year (-nn:+nn), relative to currently displayed year + // (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n) + showOtherMonths: false, // True to show dates in other months, false to leave blank + selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable + showWeek: false, // True to show week of the year, false to not show it + calculateWeek: this.iso8601Week, // How to calculate the week of the year, + // takes a Date and returns the number of the week for it + shortYearCutoff: "+10", // Short year values < this are in the current century, + // > this are in the previous century, + // string value starting with "+" for current year + value + minDate: null, // The earliest selectable date, or null for no limit + maxDate: null, // The latest selectable date, or null for no limit + duration: "fast", // Duration of display/closure + beforeShowDay: null, // Function that takes a date and returns an array with + // [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "", + // [2] = cell title (optional), e.g. $.datepicker.noWeekends + beforeShow: null, // Function that takes an input field and + // returns a set of custom settings for the date picker + onSelect: null, // Define a callback function when a date is selected + onChangeMonthYear: null, // Define a callback function when the month or year is changed + onClose: null, // Define a callback function when the datepicker is closed + numberOfMonths: 1, // Number of months to show at a time + showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0) + stepMonths: 1, // Number of months to step back/forward + stepBigMonths: 12, // Number of months to step back/forward for the big links + altField: "", // Selector for an alternate field to store selected dates into + altFormat: "", // The date format to use for the alternate field + constrainInput: true, // The input is constrained by the current date format + showButtonPanel: false, // True to show button panel, false to not show it + autoSize: false, // True to size the input for the date format, false to leave as is + disabled: false // The initial disabled state + }; + $.extend(this._defaults, this.regional[""]); + this.dpDiv = bindHover($("<div id='" + this._mainDivId + "' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")); +} + +$.extend(Datepicker.prototype, { + /* Class name added to elements to indicate already configured with a date picker. */ + markerClassName: "hasDatepicker", + + //Keep track of the maximum number of rows displayed (see #7043) + maxRows: 4, + + // TODO rename to "widget" when switching to widget factory + _widgetDatepicker: function() { + return this.dpDiv; + }, + + /* Override the default settings for all instances of the date picker. + * @param settings object - the new settings to use as defaults (anonymous object) + * @return the manager object + */ + setDefaults: function(settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* Attach the date picker to a jQuery selection. + * @param target element - the target input field or division or span + * @param settings object - the new settings to use for this date picker instance (anonymous) + */ + _attachDatepicker: function(target, settings) { + var nodeName, inline, inst; + nodeName = target.nodeName.toLowerCase(); + inline = (nodeName === "div" || nodeName === "span"); + if (!target.id) { + this.uuid += 1; + target.id = "dp" + this.uuid; + } + inst = this._newInst($(target), inline); + inst.settings = $.extend({}, settings || {}); + if (nodeName === "input") { + this._connectDatepicker(target, inst); + } else if (inline) { + this._inlineDatepicker(target, inst); + } + }, + + /* Create a new instance object. */ + _newInst: function(target, inline) { + var id = target[0].id.replace(/([^A-Za-z0-9_\-])/g, "\\\\$1"); // escape jQuery meta chars + return {id: id, input: target, // associated target + selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection + drawMonth: 0, drawYear: 0, // month being drawn + inline: inline, // is datepicker inline or not + dpDiv: (!inline ? this.dpDiv : // presentation div + bindHover($("<div class='" + this._inlineClass + " ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")))}; + }, + + /* Attach the date picker to an input field. */ + _connectDatepicker: function(target, inst) { + var input = $(target); + inst.append = $([]); + inst.trigger = $([]); + if (input.hasClass(this.markerClassName)) { + return; + } + this._attachments(input, inst); + input.addClass(this.markerClassName).keydown(this._doKeyDown). + keypress(this._doKeyPress).keyup(this._doKeyUp); + this._autoSize(inst); + $.data(target, PROP_NAME, inst); + //If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665) + if( inst.settings.disabled ) { + this._disableDatepicker( target ); + } + }, + + /* Make attachments based on settings. */ + _attachments: function(input, inst) { + var showOn, buttonText, buttonImage, + appendText = this._get(inst, "appendText"), + isRTL = this._get(inst, "isRTL"); + + if (inst.append) { + inst.append.remove(); + } + if (appendText) { + inst.append = $("<span class='" + this._appendClass + "'>" + appendText + "</span>"); + input[isRTL ? "before" : "after"](inst.append); + } + + input.unbind("focus", this._showDatepicker); + + if (inst.trigger) { + inst.trigger.remove(); + } + + showOn = this._get(inst, "showOn"); + if (showOn === "focus" || showOn === "both") { // pop-up date picker when in the marked field + input.focus(this._showDatepicker); + } + if (showOn === "button" || showOn === "both") { // pop-up date picker when button clicked + buttonText = this._get(inst, "buttonText"); + buttonImage = this._get(inst, "buttonImage"); + inst.trigger = $(this._get(inst, "buttonImageOnly") ? + $("<img/>").addClass(this._triggerClass). + attr({ src: buttonImage, alt: buttonText, title: buttonText }) : + $("<button type='button'></button>").addClass(this._triggerClass). + html(!buttonImage ? buttonText : $("<img/>").attr( + { src:buttonImage, alt:buttonText, title:buttonText }))); + input[isRTL ? "before" : "after"](inst.trigger); + inst.trigger.click(function() { + if ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) { + $.datepicker._hideDatepicker(); + } else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) { + $.datepicker._hideDatepicker(); + $.datepicker._showDatepicker(input[0]); + } else { + $.datepicker._showDatepicker(input[0]); + } + return false; + }); + } + }, + + /* Apply the maximum length for the date format. */ + _autoSize: function(inst) { + if (this._get(inst, "autoSize") && !inst.inline) { + var findMax, max, maxI, i, + date = new Date(2009, 12 - 1, 20), // Ensure double digits + dateFormat = this._get(inst, "dateFormat"); + + if (dateFormat.match(/[DM]/)) { + findMax = function(names) { + max = 0; + maxI = 0; + for (i = 0; i < names.length; i++) { + if (names[i].length > max) { + max = names[i].length; + maxI = i; + } + } + return maxI; + }; + date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ? + "monthNames" : "monthNamesShort")))); + date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ? + "dayNames" : "dayNamesShort"))) + 20 - date.getDay()); + } + inst.input.attr("size", this._formatDate(inst, date).length); + } + }, + + /* Attach an inline date picker to a div. */ + _inlineDatepicker: function(target, inst) { + var divSpan = $(target); + if (divSpan.hasClass(this.markerClassName)) { + return; + } + divSpan.addClass(this.markerClassName).append(inst.dpDiv); + $.data(target, PROP_NAME, inst); + this._setDate(inst, this._getDefaultDate(inst), true); + this._updateDatepicker(inst); + this._updateAlternate(inst); + //If disabled option is true, disable the datepicker before showing it (see ticket #5665) + if( inst.settings.disabled ) { + this._disableDatepicker( target ); + } + // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements + // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height + inst.dpDiv.css( "display", "block" ); + }, + + /* Pop-up the date picker in a "dialog" box. + * @param input element - ignored + * @param date string or Date - the initial date to display + * @param onSelect function - the function to call when a date is selected + * @param settings object - update the dialog date picker instance's settings (anonymous object) + * @param pos int[2] - coordinates for the dialog's position within the screen or + * event - with x/y coordinates or + * leave empty for default (screen centre) + * @return the manager object + */ + _dialogDatepicker: function(input, date, onSelect, settings, pos) { + var id, browserWidth, browserHeight, scrollX, scrollY, + inst = this._dialogInst; // internal instance + + if (!inst) { + this.uuid += 1; + id = "dp" + this.uuid; + this._dialogInput = $("<input type='text' id='" + id + + "' style='position: absolute; top: -100px; width: 0px;'/>"); + this._dialogInput.keydown(this._doKeyDown); + $("body").append(this._dialogInput); + inst = this._dialogInst = this._newInst(this._dialogInput, false); + inst.settings = {}; + $.data(this._dialogInput[0], PROP_NAME, inst); + } + extendRemove(inst.settings, settings || {}); + date = (date && date.constructor === Date ? this._formatDate(inst, date) : date); + this._dialogInput.val(date); + + this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null); + if (!this._pos) { + browserWidth = document.documentElement.clientWidth; + browserHeight = document.documentElement.clientHeight; + scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; + scrollY = document.documentElement.scrollTop || document.body.scrollTop; + this._pos = // should use actual width/height below + [(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY]; + } + + // move input on screen for focus, but hidden behind dialog + this._dialogInput.css("left", (this._pos[0] + 20) + "px").css("top", this._pos[1] + "px"); + inst.settings.onSelect = onSelect; + this._inDialog = true; + this.dpDiv.addClass(this._dialogClass); + this._showDatepicker(this._dialogInput[0]); + if ($.blockUI) { + $.blockUI(this.dpDiv); + } + $.data(this._dialogInput[0], PROP_NAME, inst); + return this; + }, + + /* Detach a datepicker from its control. + * @param target element - the target input field or division or span + */ + _destroyDatepicker: function(target) { + var nodeName, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + $.removeData(target, PROP_NAME); + if (nodeName === "input") { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass(this.markerClassName). + unbind("focus", this._showDatepicker). + unbind("keydown", this._doKeyDown). + unbind("keypress", this._doKeyPress). + unbind("keyup", this._doKeyUp); + } else if (nodeName === "div" || nodeName === "span") { + $target.removeClass(this.markerClassName).empty(); + } + }, + + /* Enable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _enableDatepicker: function(target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = false; + inst.trigger.filter("button"). + each(function() { this.disabled = false; }).end(). + filter("img").css({opacity: "1.0", cursor: ""}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().removeClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). + prop("disabled", false); + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value === target ? null : value); }); // delete entry + }, + + /* Disable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _disableDatepicker: function(target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = true; + inst.trigger.filter("button"). + each(function() { this.disabled = true; }).end(). + filter("img").css({opacity: "0.5", cursor: "default"}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().addClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). + prop("disabled", true); + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value === target ? null : value); }); // delete entry + this._disabledInputs[this._disabledInputs.length] = target; + }, + + /* Is the first field in a jQuery collection disabled as a datepicker? + * @param target element - the target input field or division or span + * @return boolean - true if disabled, false if enabled + */ + _isDisabledDatepicker: function(target) { + if (!target) { + return false; + } + for (var i = 0; i < this._disabledInputs.length; i++) { + if (this._disabledInputs[i] === target) { + return true; + } + } + return false; + }, + + /* Retrieve the instance data for the target control. + * @param target element - the target input field or division or span + * @return object - the associated instance data + * @throws error if a jQuery problem getting data + */ + _getInst: function(target) { + try { + return $.data(target, PROP_NAME); + } + catch (err) { + throw "Missing instance data for this datepicker"; + } + }, + + /* Update or retrieve the settings for a date picker attached to an input field or division. + * @param target element - the target input field or division or span + * @param name object - the new settings to update or + * string - the name of the setting to change or retrieve, + * when retrieving also "all" for all instance settings or + * "defaults" for all global defaults + * @param value any - the new value for the setting + * (omit if above is an object or to retrieve a value) + */ + _optionDatepicker: function(target, name, value) { + var settings, date, minDate, maxDate, + inst = this._getInst(target); + + if (arguments.length === 2 && typeof name === "string") { + return (name === "defaults" ? $.extend({}, $.datepicker._defaults) : + (inst ? (name === "all" ? $.extend({}, inst.settings) : + this._get(inst, name)) : null)); + } + + settings = name || {}; + if (typeof name === "string") { + settings = {}; + settings[name] = value; + } + + if (inst) { + if (this._curInst === inst) { + this._hideDatepicker(); + } + + date = this._getDateDatepicker(target, true); + minDate = this._getMinMaxDate(inst, "min"); + maxDate = this._getMinMaxDate(inst, "max"); + extendRemove(inst.settings, settings); + // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided + if (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) { + inst.settings.minDate = this._formatDate(inst, minDate); + } + if (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) { + inst.settings.maxDate = this._formatDate(inst, maxDate); + } + if ( "disabled" in settings ) { + if ( settings.disabled ) { + this._disableDatepicker(target); + } else { + this._enableDatepicker(target); + } + } + this._attachments($(target), inst); + this._autoSize(inst); + this._setDate(inst, date); + this._updateAlternate(inst); + this._updateDatepicker(inst); + } + }, + + // change method deprecated + _changeDatepicker: function(target, name, value) { + this._optionDatepicker(target, name, value); + }, + + /* Redraw the date picker attached to an input field or division. + * @param target element - the target input field or division or span + */ + _refreshDatepicker: function(target) { + var inst = this._getInst(target); + if (inst) { + this._updateDatepicker(inst); + } + }, + + /* Set the dates for a jQuery selection. + * @param target element - the target input field or division or span + * @param date Date - the new date + */ + _setDateDatepicker: function(target, date) { + var inst = this._getInst(target); + if (inst) { + this._setDate(inst, date); + this._updateDatepicker(inst); + this._updateAlternate(inst); + } + }, + + /* Get the date(s) for the first entry in a jQuery selection. + * @param target element - the target input field or division or span + * @param noDefault boolean - true if no default date is to be used + * @return Date - the current date + */ + _getDateDatepicker: function(target, noDefault) { + var inst = this._getInst(target); + if (inst && !inst.inline) { + this._setDateFromField(inst, noDefault); + } + return (inst ? this._getDate(inst) : null); + }, + + /* Handle keystrokes. */ + _doKeyDown: function(event) { + var onSelect, dateStr, sel, + inst = $.datepicker._getInst(event.target), + handled = true, + isRTL = inst.dpDiv.is(".ui-datepicker-rtl"); + + inst._keyEvent = true; + if ($.datepicker._datepickerShowing) { + switch (event.keyCode) { + case 9: $.datepicker._hideDatepicker(); + handled = false; + break; // hide on tab out + case 13: sel = $("td." + $.datepicker._dayOverClass + ":not(." + + $.datepicker._currentClass + ")", inst.dpDiv); + if (sel[0]) { + $.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]); + } + + onSelect = $.datepicker._get(inst, "onSelect"); + if (onSelect) { + dateStr = $.datepicker._formatDate(inst); + + // trigger custom callback + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); + } else { + $.datepicker._hideDatepicker(); + } + + return false; // don't submit the form + case 27: $.datepicker._hideDatepicker(); + break; // hide on escape + case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + break; // previous month/year on page up/+ ctrl + case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + break; // next month/year on page down/+ ctrl + case 35: if (event.ctrlKey || event.metaKey) { + $.datepicker._clearDate(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // clear on ctrl or command +end + case 36: if (event.ctrlKey || event.metaKey) { + $.datepicker._gotoToday(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // current on ctrl or command +home + case 37: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // -1 day on ctrl or command +left + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +left on Mac + break; + case 38: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, -7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // -1 week on ctrl or command +up + case 39: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // +1 day on ctrl or command +right + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +right + break; + case 40: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, +7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // +1 week on ctrl or command +down + default: handled = false; + } + } else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home + $.datepicker._showDatepicker(this); + } else { + handled = false; + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + /* Filter entered characters - based on date format. */ + _doKeyPress: function(event) { + var chars, chr, + inst = $.datepicker._getInst(event.target); + + if ($.datepicker._get(inst, "constrainInput")) { + chars = $.datepicker._possibleChars($.datepicker._get(inst, "dateFormat")); + chr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode); + return event.ctrlKey || event.metaKey || (chr < " " || !chars || chars.indexOf(chr) > -1); + } + }, + + /* Synchronise manual entry and field/alternate field. */ + _doKeyUp: function(event) { + var date, + inst = $.datepicker._getInst(event.target); + + if (inst.input.val() !== inst.lastVal) { + try { + date = $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + (inst.input ? inst.input.val() : null), + $.datepicker._getFormatConfig(inst)); + + if (date) { // only if valid + $.datepicker._setDateFromField(inst); + $.datepicker._updateAlternate(inst); + $.datepicker._updateDatepicker(inst); + } + } + catch (err) { + } + } + return true; + }, + + /* Pop-up the date picker for a given input field. + * If false returned from beforeShow event handler do not show. + * @param input element - the input field attached to the date picker or + * event - if triggered by focus + */ + _showDatepicker: function(input) { + input = input.target || input; + if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger + input = $("input", input.parentNode)[0]; + } + + if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here + return; + } + + var inst, beforeShow, beforeShowSettings, isFixed, + offset, showAnim, duration; + + inst = $.datepicker._getInst(input); + if ($.datepicker._curInst && $.datepicker._curInst !== inst) { + $.datepicker._curInst.dpDiv.stop(true, true); + if ( inst && $.datepicker._datepickerShowing ) { + $.datepicker._hideDatepicker( $.datepicker._curInst.input[0] ); + } + } + + beforeShow = $.datepicker._get(inst, "beforeShow"); + beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {}; + if(beforeShowSettings === false){ + return; + } + extendRemove(inst.settings, beforeShowSettings); + + inst.lastVal = null; + $.datepicker._lastInput = input; + $.datepicker._setDateFromField(inst); + + if ($.datepicker._inDialog) { // hide cursor + input.value = ""; + } + if (!$.datepicker._pos) { // position below input + $.datepicker._pos = $.datepicker._findPos(input); + $.datepicker._pos[1] += input.offsetHeight; // add the height + } + + isFixed = false; + $(input).parents().each(function() { + isFixed |= $(this).css("position") === "fixed"; + return !isFixed; + }); + + offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]}; + $.datepicker._pos = null; + //to avoid flashes on Firefox + inst.dpDiv.empty(); + // determine sizing offscreen + inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"}); + $.datepicker._updateDatepicker(inst); + // fix width for dynamic number of date pickers + // and adjust position before showing + offset = $.datepicker._checkOffset(inst, offset, isFixed); + inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ? + "static" : (isFixed ? "fixed" : "absolute")), display: "none", + left: offset.left + "px", top: offset.top + "px"}); + + if (!inst.inline) { + showAnim = $.datepicker._get(inst, "showAnim"); + duration = $.datepicker._get(inst, "duration"); + inst.dpDiv.zIndex($(input).zIndex()+1); + $.datepicker._datepickerShowing = true; + + if ( $.effects && $.effects.effect[ showAnim ] ) { + inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration); + } else { + inst.dpDiv[showAnim || "show"](showAnim ? duration : null); + } + + if (inst.input.is(":visible") && !inst.input.is(":disabled")) { + inst.input.focus(); + } + $.datepicker._curInst = inst; + } + }, + + /* Generate the date picker content. */ + _updateDatepicker: function(inst) { + this.maxRows = 4; //Reset the max number of rows being displayed (see #7043) + instActive = inst; // for delegate hover events + inst.dpDiv.empty().append(this._generateHTML(inst)); + this._attachHandlers(inst); + inst.dpDiv.find("." + this._dayOverClass + " a").mouseover(); + + var origyearshtml, + numMonths = this._getNumberOfMonths(inst), + cols = numMonths[1], + width = 17; + + inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""); + if (cols > 1) { + inst.dpDiv.addClass("ui-datepicker-multi-" + cols).css("width", (width * cols) + "em"); + } + inst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? "add" : "remove") + + "Class"]("ui-datepicker-multi"); + inst.dpDiv[(this._get(inst, "isRTL") ? "add" : "remove") + + "Class"]("ui-datepicker-rtl"); + + // #6694 - don't focus the input if it's already focused + // this breaks the change event in IE + if (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && inst.input && + inst.input.is(":visible") && !inst.input.is(":disabled") && inst.input[0] !== document.activeElement) { + inst.input.focus(); + } + + // deffered render of the years select (to avoid flashes on Firefox) + if( inst.yearshtml ){ + origyearshtml = inst.yearshtml; + setTimeout(function(){ + //assure that inst.yearshtml didn't change. + if( origyearshtml === inst.yearshtml && inst.yearshtml ){ + inst.dpDiv.find("select.ui-datepicker-year:first").replaceWith(inst.yearshtml); + } + origyearshtml = inst.yearshtml = null; + }, 0); + } + }, + + /* Retrieve the size of left and top borders for an element. + * @param elem (jQuery object) the element of interest + * @return (number[2]) the left and top borders + */ + _getBorders: function(elem) { + var convert = function(value) { + return {thin: 1, medium: 2, thick: 3}[value] || value; + }; + return [parseFloat(convert(elem.css("border-left-width"))), + parseFloat(convert(elem.css("border-top-width")))]; + }, + + /* Check positioning to remain on screen. */ + _checkOffset: function(inst, offset, isFixed) { + var dpWidth = inst.dpDiv.outerWidth(), + dpHeight = inst.dpDiv.outerHeight(), + inputWidth = inst.input ? inst.input.outerWidth() : 0, + inputHeight = inst.input ? inst.input.outerHeight() : 0, + viewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()), + viewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop()); + + offset.left -= (this._get(inst, "isRTL") ? (dpWidth - inputWidth) : 0); + offset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0; + offset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; + + // now check if datepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? + Math.abs(offset.left + dpWidth - viewWidth) : 0); + offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? + Math.abs(dpHeight + inputHeight) : 0); + + return offset; + }, + + /* Find an object's position on the screen. */ + _findPos: function(obj) { + var position, + inst = this._getInst(obj), + isRTL = this._get(inst, "isRTL"); + + while (obj && (obj.type === "hidden" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) { + obj = obj[isRTL ? "previousSibling" : "nextSibling"]; + } + + position = $(obj).offset(); + return [position.left, position.top]; + }, + + /* Hide the date picker from view. + * @param input element - the input field attached to the date picker + */ + _hideDatepicker: function(input) { + var showAnim, duration, postProcess, onClose, + inst = this._curInst; + + if (!inst || (input && inst !== $.data(input, PROP_NAME))) { + return; + } + + if (this._datepickerShowing) { + showAnim = this._get(inst, "showAnim"); + duration = this._get(inst, "duration"); + postProcess = function() { + $.datepicker._tidyDialog(inst); + }; + + // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed + if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) { + inst.dpDiv.hide(showAnim, $.datepicker._get(inst, "showOptions"), duration, postProcess); + } else { + inst.dpDiv[(showAnim === "slideDown" ? "slideUp" : + (showAnim === "fadeIn" ? "fadeOut" : "hide"))]((showAnim ? duration : null), postProcess); + } + + if (!showAnim) { + postProcess(); + } + this._datepickerShowing = false; + + onClose = this._get(inst, "onClose"); + if (onClose) { + onClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : ""), inst]); + } + + this._lastInput = null; + if (this._inDialog) { + this._dialogInput.css({ position: "absolute", left: "0", top: "-100px" }); + if ($.blockUI) { + $.unblockUI(); + $("body").append(this.dpDiv); + } + } + this._inDialog = false; + } + }, + + /* Tidy up after a dialog display. */ + _tidyDialog: function(inst) { + inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar"); + }, + + /* Close date picker if clicked elsewhere. */ + _checkExternalClick: function(event) { + if (!$.datepicker._curInst) { + return; + } + + var $target = $(event.target), + inst = $.datepicker._getInst($target[0]); + + if ( ( ( $target[0].id !== $.datepicker._mainDivId && + $target.parents("#" + $.datepicker._mainDivId).length === 0 && + !$target.hasClass($.datepicker.markerClassName) && + !$target.closest("." + $.datepicker._triggerClass).length && + $.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) || + ( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst ) ) { + $.datepicker._hideDatepicker(); + } + }, + + /* Adjust one of the date sub-fields. */ + _adjustDate: function(id, offset, period) { + var target = $(id), + inst = this._getInst(target[0]); + + if (this._isDisabledDatepicker(target[0])) { + return; + } + this._adjustInstDate(inst, offset + + (period === "M" ? this._get(inst, "showCurrentAtPos") : 0), // undo positioning + period); + this._updateDatepicker(inst); + }, + + /* Action for current link. */ + _gotoToday: function(id) { + var date, + target = $(id), + inst = this._getInst(target[0]); + + if (this._get(inst, "gotoCurrent") && inst.currentDay) { + inst.selectedDay = inst.currentDay; + inst.drawMonth = inst.selectedMonth = inst.currentMonth; + inst.drawYear = inst.selectedYear = inst.currentYear; + } else { + date = new Date(); + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + } + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a new month/year. */ + _selectMonthYear: function(id, select, period) { + var target = $(id), + inst = this._getInst(target[0]); + + inst["selected" + (period === "M" ? "Month" : "Year")] = + inst["draw" + (period === "M" ? "Month" : "Year")] = + parseInt(select.options[select.selectedIndex].value,10); + + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a day. */ + _selectDay: function(id, month, year, td) { + var inst, + target = $(id); + + if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) { + return; + } + + inst = this._getInst(target[0]); + inst.selectedDay = inst.currentDay = $("a", td).html(); + inst.selectedMonth = inst.currentMonth = month; + inst.selectedYear = inst.currentYear = year; + this._selectDate(id, this._formatDate(inst, + inst.currentDay, inst.currentMonth, inst.currentYear)); + }, + + /* Erase the input field and hide the date picker. */ + _clearDate: function(id) { + var target = $(id); + this._selectDate(target, ""); + }, + + /* Update the input field with the selected date. */ + _selectDate: function(id, dateStr) { + var onSelect, + target = $(id), + inst = this._getInst(target[0]); + + dateStr = (dateStr != null ? dateStr : this._formatDate(inst)); + if (inst.input) { + inst.input.val(dateStr); + } + this._updateAlternate(inst); + + onSelect = this._get(inst, "onSelect"); + if (onSelect) { + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback + } else if (inst.input) { + inst.input.trigger("change"); // fire the change event + } + + if (inst.inline){ + this._updateDatepicker(inst); + } else { + this._hideDatepicker(); + this._lastInput = inst.input[0]; + if (typeof(inst.input[0]) !== "object") { + inst.input.focus(); // restore focus + } + this._lastInput = null; + } + }, + + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function(inst) { + var altFormat, date, dateStr, + altField = this._get(inst, "altField"); + + if (altField) { // update alternate field too + altFormat = this._get(inst, "altFormat") || this._get(inst, "dateFormat"); + date = this._getDate(inst); + dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst)); + $(altField).each(function() { $(this).val(dateStr); }); + } + }, + + /* Set as beforeShowDay function to prevent selection of weekends. + * @param date Date - the date to customise + * @return [boolean, string] - is this date selectable?, what is its CSS class? + */ + noWeekends: function(date) { + var day = date.getDay(); + return [(day > 0 && day < 6), ""]; + }, + + /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * @param date Date - the date to get the week for + * @return number - the number of the week within the year that contains this date + */ + iso8601Week: function(date) { + var time, + checkDate = new Date(date.getTime()); + + // Find Thursday of this week starting on Monday + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); + + time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + }, + + /* Parse a string value into a date object. + * See formatDate below for the possible formats. + * + * @param format string - the expected format of the date + * @param value string - the date in the above format + * @param settings Object - attributes include: + * shortYearCutoff number - the cutoff year for determining the century (optional) + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return Date - the extracted date value or null if value is blank + */ + parseDate: function (format, value, settings) { + if (format == null || value == null) { + throw "Invalid arguments"; + } + + value = (typeof value === "object" ? value.toString() : value + ""); + if (value === "") { + return null; + } + + var iFormat, dim, extra, + iValue = 0, + shortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff, + shortYearCutoff = (typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp : + new Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)), + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + year = -1, + month = -1, + day = -1, + doy = -1, + literal = false, + date, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Extract a number from the string value + getNumber = function(match) { + var isDoubled = lookAhead(match), + size = (match === "@" ? 14 : (match === "!" ? 20 : + (match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))), + digits = new RegExp("^\\d{1," + size + "}"), + num = value.substring(iValue).match(digits); + if (!num) { + throw "Missing number at position " + iValue; + } + iValue += num[0].length; + return parseInt(num[0], 10); + }, + // Extract a name from the string value and convert to an index + getName = function(match, shortNames, longNames) { + var index = -1, + names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) { + return [ [k, v] ]; + }).sort(function (a, b) { + return -(a[1].length - b[1].length); + }); + + $.each(names, function (i, pair) { + var name = pair[1]; + if (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) { + index = pair[0]; + iValue += name.length; + return false; + } + }); + if (index !== -1) { + return index + 1; + } else { + throw "Unknown name at position " + iValue; + } + }, + // Confirm that a literal character matches the string value + checkLiteral = function() { + if (value.charAt(iValue) !== format.charAt(iFormat)) { + throw "Unexpected literal at position " + iValue; + } + iValue++; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + checkLiteral(); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + day = getNumber("d"); + break; + case "D": + getName("D", dayNamesShort, dayNames); + break; + case "o": + doy = getNumber("o"); + break; + case "m": + month = getNumber("m"); + break; + case "M": + month = getName("M", monthNamesShort, monthNames); + break; + case "y": + year = getNumber("y"); + break; + case "@": + date = new Date(getNumber("@")); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "!": + date = new Date((getNumber("!") - this._ticksTo1970) / 10000); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "'": + if (lookAhead("'")){ + checkLiteral(); + } else { + literal = true; + } + break; + default: + checkLiteral(); + } + } + } + + if (iValue < value.length){ + extra = value.substr(iValue); + if (!/^\s+/.test(extra)) { + throw "Extra/unparsed characters found in date: " + extra; + } + } + + if (year === -1) { + year = new Date().getFullYear(); + } else if (year < 100) { + year += new Date().getFullYear() - new Date().getFullYear() % 100 + + (year <= shortYearCutoff ? 0 : -100); + } + + if (doy > -1) { + month = 1; + day = doy; + do { + dim = this._getDaysInMonth(year, month - 1); + if (day <= dim) { + break; + } + month++; + day -= dim; + } while (true); + } + + date = this._daylightSavingAdjust(new Date(year, month - 1, day)); + if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) { + throw "Invalid date"; // E.g. 31/02/00 + } + return date; + }, + + /* Standard date formats. */ + ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601) + COOKIE: "D, dd M yy", + ISO_8601: "yy-mm-dd", + RFC_822: "D, d M y", + RFC_850: "DD, dd-M-y", + RFC_1036: "D, d M y", + RFC_1123: "D, d M yy", + RFC_2822: "D, d M yy", + RSS: "D, d M y", // RFC 822 + TICKS: "!", + TIMESTAMP: "@", + W3C: "yy-mm-dd", // ISO 8601 + + _ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + + Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000), + + /* Format a date object into a string value. + * The format can be combinations of the following: + * d - day of month (no leading zero) + * dd - day of month (two digit) + * o - day of year (no leading zeros) + * oo - day of year (three digit) + * D - day name short + * DD - day name long + * m - month of year (no leading zero) + * mm - month of year (two digit) + * M - month name short + * MM - month name long + * y - year (two digit) + * yy - year (four digit) + * @ - Unix timestamp (ms since 01/01/1970) + * ! - Windows ticks (100ns since 01/01/0001) + * "..." - literal text + * '' - single quote + * + * @param format string - the desired format of the date + * @param date Date - the date value to format + * @param settings Object - attributes include: + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return string - the date in the above format + */ + formatDate: function (format, date, settings) { + if (!date) { + return ""; + } + + var iFormat, + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Format a number, with leading zero if necessary + formatNumber = function(match, value, len) { + var num = "" + value; + if (lookAhead(match)) { + while (num.length < len) { + num = "0" + num; + } + } + return num; + }, + // Format a name, short or long as requested + formatName = function(match, value, shortNames, longNames) { + return (lookAhead(match) ? longNames[value] : shortNames[value]); + }, + output = "", + literal = false; + + if (date) { + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + output += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + output += formatNumber("d", date.getDate(), 2); + break; + case "D": + output += formatName("D", date.getDay(), dayNamesShort, dayNames); + break; + case "o": + output += formatNumber("o", + Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3); + break; + case "m": + output += formatNumber("m", date.getMonth() + 1, 2); + break; + case "M": + output += formatName("M", date.getMonth(), monthNamesShort, monthNames); + break; + case "y": + output += (lookAhead("y") ? date.getFullYear() : + (date.getYear() % 100 < 10 ? "0" : "") + date.getYear() % 100); + break; + case "@": + output += date.getTime(); + break; + case "!": + output += date.getTime() * 10000 + this._ticksTo1970; + break; + case "'": + if (lookAhead("'")) { + output += "'"; + } else { + literal = true; + } + break; + default: + output += format.charAt(iFormat); + } + } + } + } + return output; + }, + + /* Extract all possible characters from the date format. */ + _possibleChars: function (format) { + var iFormat, + chars = "", + literal = false, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + chars += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": case "m": case "y": case "@": + chars += "0123456789"; + break; + case "D": case "M": + return null; // Accept anything + case "'": + if (lookAhead("'")) { + chars += "'"; + } else { + literal = true; + } + break; + default: + chars += format.charAt(iFormat); + } + } + } + return chars; + }, + + /* Get a setting value, defaulting if necessary. */ + _get: function(inst, name) { + return inst.settings[name] !== undefined ? + inst.settings[name] : this._defaults[name]; + }, + + /* Parse existing date and initialise date picker. */ + _setDateFromField: function(inst, noDefault) { + if (inst.input.val() === inst.lastVal) { + return; + } + + var dateFormat = this._get(inst, "dateFormat"), + dates = inst.lastVal = inst.input ? inst.input.val() : null, + defaultDate = this._getDefaultDate(inst), + date = defaultDate, + settings = this._getFormatConfig(inst); + + try { + date = this.parseDate(dateFormat, dates, settings) || defaultDate; + } catch (event) { + dates = (noDefault ? "" : dates); + } + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + inst.currentDay = (dates ? date.getDate() : 0); + inst.currentMonth = (dates ? date.getMonth() : 0); + inst.currentYear = (dates ? date.getFullYear() : 0); + this._adjustInstDate(inst); + }, + + /* Retrieve the default date shown on opening. */ + _getDefaultDate: function(inst) { + return this._restrictMinMax(inst, + this._determineDate(inst, this._get(inst, "defaultDate"), new Date())); + }, + + /* A date may be specified as an exact value or a relative one. */ + _determineDate: function(inst, date, defaultDate) { + var offsetNumeric = function(offset) { + var date = new Date(); + date.setDate(date.getDate() + offset); + return date; + }, + offsetString = function(offset) { + try { + return $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + offset, $.datepicker._getFormatConfig(inst)); + } + catch (e) { + // Ignore + } + + var date = (offset.toLowerCase().match(/^c/) ? + $.datepicker._getDate(inst) : null) || new Date(), + year = date.getFullYear(), + month = date.getMonth(), + day = date.getDate(), + pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, + matches = pattern.exec(offset); + + while (matches) { + switch (matches[2] || "d") { + case "d" : case "D" : + day += parseInt(matches[1],10); break; + case "w" : case "W" : + day += parseInt(matches[1],10) * 7; break; + case "m" : case "M" : + month += parseInt(matches[1],10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + case "y": case "Y" : + year += parseInt(matches[1],10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + } + matches = pattern.exec(offset); + } + return new Date(year, month, day); + }, + newDate = (date == null || date === "" ? defaultDate : (typeof date === "string" ? offsetString(date) : + (typeof date === "number" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime())))); + + newDate = (newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate); + if (newDate) { + newDate.setHours(0); + newDate.setMinutes(0); + newDate.setSeconds(0); + newDate.setMilliseconds(0); + } + return this._daylightSavingAdjust(newDate); + }, + + /* Handle switch to/from daylight saving. + * Hours may be non-zero on daylight saving cut-over: + * > 12 when midnight changeover, but then cannot generate + * midnight datetime, so jump to 1AM, otherwise reset. + * @param date (Date) the date to check + * @return (Date) the corrected date + */ + _daylightSavingAdjust: function(date) { + if (!date) { + return null; + } + date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0); + return date; + }, + + /* Set the date(s) directly. */ + _setDate: function(inst, date, noChange) { + var clear = !date, + origMonth = inst.selectedMonth, + origYear = inst.selectedYear, + newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date())); + + inst.selectedDay = inst.currentDay = newDate.getDate(); + inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); + inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); + if ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) { + this._notifyChange(inst); + } + this._adjustInstDate(inst); + if (inst.input) { + inst.input.val(clear ? "" : this._formatDate(inst)); + } + }, + + /* Retrieve the date(s) directly. */ + _getDate: function(inst) { + var startDate = (!inst.currentYear || (inst.input && inst.input.val() === "") ? null : + this._daylightSavingAdjust(new Date( + inst.currentYear, inst.currentMonth, inst.currentDay))); + return startDate; + }, + + /* Attach the onxxx handlers. These are declared statically so + * they work with static code transformers like Caja. + */ + _attachHandlers: function(inst) { + var stepMonths = this._get(inst, "stepMonths"), + id = "#" + inst.id.replace( /\\\\/g, "\\" ); + inst.dpDiv.find("[data-handler]").map(function () { + var handler = { + prev: function () { + window["DP_jQuery_" + dpuuid].datepicker._adjustDate(id, -stepMonths, "M"); + }, + next: function () { + window["DP_jQuery_" + dpuuid].datepicker._adjustDate(id, +stepMonths, "M"); + }, + hide: function () { + window["DP_jQuery_" + dpuuid].datepicker._hideDatepicker(); + }, + today: function () { + window["DP_jQuery_" + dpuuid].datepicker._gotoToday(id); + }, + selectDay: function () { + window["DP_jQuery_" + dpuuid].datepicker._selectDay(id, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this); + return false; + }, + selectMonth: function () { + window["DP_jQuery_" + dpuuid].datepicker._selectMonthYear(id, this, "M"); + return false; + }, + selectYear: function () { + window["DP_jQuery_" + dpuuid].datepicker._selectMonthYear(id, this, "Y"); + return false; + } + }; + $(this).bind(this.getAttribute("data-event"), handler[this.getAttribute("data-handler")]); + }); + }, + + /* Generate the HTML for the current state of the date picker. */ + _generateHTML: function(inst) { + var maxDraw, prevText, prev, nextText, next, currentText, gotoDate, + controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin, + monthNames, monthNamesShort, beforeShowDay, showOtherMonths, + selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate, + cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows, + printDate, dRow, tbody, daySettings, otherMonth, unselectable, + tempDate = new Date(), + today = this._daylightSavingAdjust( + new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time + isRTL = this._get(inst, "isRTL"), + showButtonPanel = this._get(inst, "showButtonPanel"), + hideIfNoPrevNext = this._get(inst, "hideIfNoPrevNext"), + navigationAsDateFormat = this._get(inst, "navigationAsDateFormat"), + numMonths = this._getNumberOfMonths(inst), + showCurrentAtPos = this._get(inst, "showCurrentAtPos"), + stepMonths = this._get(inst, "stepMonths"), + isMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1), + currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) : + new Date(inst.currentYear, inst.currentMonth, inst.currentDay))), + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + drawMonth = inst.drawMonth - showCurrentAtPos, + drawYear = inst.drawYear; + + if (drawMonth < 0) { + drawMonth += 12; + drawYear--; + } + if (maxDate) { + maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(), + maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate())); + maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw); + while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) { + drawMonth--; + if (drawMonth < 0) { + drawMonth = 11; + drawYear--; + } + } + } + inst.drawMonth = drawMonth; + inst.drawYear = drawYear; + + prevText = this._get(inst, "prevText"); + prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)), + this._getFormatConfig(inst))); + + prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ? + "<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click'" + + " title='" + prevText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>" : + (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+ prevText +"'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>")); + + nextText = this._get(inst, "nextText"); + nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)), + this._getFormatConfig(inst))); + + next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ? + "<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click'" + + " title='" + nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>" : + (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+ nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>")); + + currentText = this._get(inst, "currentText"); + gotoDate = (this._get(inst, "gotoCurrent") && inst.currentDay ? currentDate : today); + currentText = (!navigationAsDateFormat ? currentText : + this.formatDate(currentText, gotoDate, this._getFormatConfig(inst))); + + controls = (!inst.inline ? "<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>" + + this._get(inst, "closeText") + "</button>" : ""); + + buttonPanel = (showButtonPanel) ? "<div class='ui-datepicker-buttonpane ui-widget-content'>" + (isRTL ? controls : "") + + (this._isInRange(inst, gotoDate) ? "<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'" + + ">" + currentText + "</button>" : "") + (isRTL ? "" : controls) + "</div>" : ""; + + firstDay = parseInt(this._get(inst, "firstDay"),10); + firstDay = (isNaN(firstDay) ? 0 : firstDay); + + showWeek = this._get(inst, "showWeek"); + dayNames = this._get(inst, "dayNames"); + dayNamesMin = this._get(inst, "dayNamesMin"); + monthNames = this._get(inst, "monthNames"); + monthNamesShort = this._get(inst, "monthNamesShort"); + beforeShowDay = this._get(inst, "beforeShowDay"); + showOtherMonths = this._get(inst, "showOtherMonths"); + selectOtherMonths = this._get(inst, "selectOtherMonths"); + defaultDate = this._getDefaultDate(inst); + html = ""; + dow; + for (row = 0; row < numMonths[0]; row++) { + group = ""; + this.maxRows = 4; + for (col = 0; col < numMonths[1]; col++) { + selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay)); + cornerClass = " ui-corner-all"; + calender = ""; + if (isMultiMonth) { + calender += "<div class='ui-datepicker-group"; + if (numMonths[1] > 1) { + switch (col) { + case 0: calender += " ui-datepicker-group-first"; + cornerClass = " ui-corner-" + (isRTL ? "right" : "left"); break; + case numMonths[1]-1: calender += " ui-datepicker-group-last"; + cornerClass = " ui-corner-" + (isRTL ? "left" : "right"); break; + default: calender += " ui-datepicker-group-middle"; cornerClass = ""; break; + } + } + calender += "'>"; + } + calender += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + cornerClass + "'>" + + (/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : "") + + (/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : "") + + this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate, + row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers + "</div><table class='ui-datepicker-calendar'><thead>" + + "<tr>"; + thead = (showWeek ? "<th class='ui-datepicker-week-col'>" + this._get(inst, "weekHeader") + "</th>" : ""); + for (dow = 0; dow < 7; dow++) { // days of the week + day = (dow + firstDay) % 7; + thead += "<th" + ((dow + firstDay + 6) % 7 >= 5 ? " class='ui-datepicker-week-end'" : "") + ">" + + "<span title='" + dayNames[day] + "'>" + dayNamesMin[day] + "</span></th>"; + } + calender += thead + "</tr></thead><tbody>"; + daysInMonth = this._getDaysInMonth(drawYear, drawMonth); + if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) { + inst.selectedDay = Math.min(inst.selectedDay, daysInMonth); + } + leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7; + curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate + numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043) + this.maxRows = numRows; + printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays)); + for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows + calender += "<tr>"; + tbody = (!showWeek ? "" : "<td class='ui-datepicker-week-col'>" + + this._get(inst, "calculateWeek")(printDate) + "</td>"); + for (dow = 0; dow < 7; dow++) { // create date picker days + daySettings = (beforeShowDay ? + beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, ""]); + otherMonth = (printDate.getMonth() !== drawMonth); + unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] || + (minDate && printDate < minDate) || (maxDate && printDate > maxDate); + tbody += "<td class='" + + ((dow + firstDay + 6) % 7 >= 5 ? " ui-datepicker-week-end" : "") + // highlight weekends + (otherMonth ? " ui-datepicker-other-month" : "") + // highlight days from other months + ((printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent) || // user pressed key + (defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime()) ? + // or defaultDate is current printedDate and defaultDate is selectedDate + " " + this._dayOverClass : "") + // highlight selected day + (unselectable ? " " + this._unselectableClass + " ui-state-disabled": "") + // highlight unselectable days + (otherMonth && !showOtherMonths ? "" : " " + daySettings[1] + // highlight custom dates + (printDate.getTime() === currentDate.getTime() ? " " + this._currentClass : "") + // highlight selected day + (printDate.getTime() === today.getTime() ? " ui-datepicker-today" : "")) + "'" + // highlight today (if different) + ((!otherMonth || showOtherMonths) && daySettings[2] ? " title='" + daySettings[2].replace(/'/g, "'") + "'" : "") + // cell title + (unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'") + ">" + // actions + (otherMonth && !showOtherMonths ? " " : // display for other months + (unselectable ? "<span class='ui-state-default'>" + printDate.getDate() + "</span>" : "<a class='ui-state-default" + + (printDate.getTime() === today.getTime() ? " ui-state-highlight" : "") + + (printDate.getTime() === currentDate.getTime() ? " ui-state-active" : "") + // highlight selected day + (otherMonth ? " ui-priority-secondary" : "") + // distinguish dates from other months + "' href='#'>" + printDate.getDate() + "</a>")) + "</td>"; // display selectable date + printDate.setDate(printDate.getDate() + 1); + printDate = this._daylightSavingAdjust(printDate); + } + calender += tbody + "</tr>"; + } + drawMonth++; + if (drawMonth > 11) { + drawMonth = 0; + drawYear++; + } + calender += "</tbody></table>" + (isMultiMonth ? "</div>" + + ((numMonths[0] > 0 && col === numMonths[1]-1) ? "<div class='ui-datepicker-row-break'></div>" : "") : ""); + group += calender; + } + html += group; + } + html += buttonPanel; + inst._keyEvent = false; + return html; + }, + + /* Generate the month and year header. */ + _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate, + secondary, monthNames, monthNamesShort) { + + var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear, + changeMonth = this._get(inst, "changeMonth"), + changeYear = this._get(inst, "changeYear"), + showMonthAfterYear = this._get(inst, "showMonthAfterYear"), + html = "<div class='ui-datepicker-title'>", + monthHtml = ""; + + // month selection + if (secondary || !changeMonth) { + monthHtml += "<span class='ui-datepicker-month'>" + monthNames[drawMonth] + "</span>"; + } else { + inMinYear = (minDate && minDate.getFullYear() === drawYear); + inMaxYear = (maxDate && maxDate.getFullYear() === drawYear); + monthHtml += "<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>"; + for ( month = 0; month < 12; month++) { + if ((!inMinYear || month >= minDate.getMonth()) && (!inMaxYear || month <= maxDate.getMonth())) { + monthHtml += "<option value='" + month + "'" + + (month === drawMonth ? " selected='selected'" : "") + + ">" + monthNamesShort[month] + "</option>"; + } + } + monthHtml += "</select>"; + } + + if (!showMonthAfterYear) { + html += monthHtml + (secondary || !(changeMonth && changeYear) ? " " : ""); + } + + // year selection + if ( !inst.yearshtml ) { + inst.yearshtml = ""; + if (secondary || !changeYear) { + html += "<span class='ui-datepicker-year'>" + drawYear + "</span>"; + } else { + // determine range of years to display + years = this._get(inst, "yearRange").split(":"); + thisYear = new Date().getFullYear(); + determineYear = function(value) { + var year = (value.match(/c[+\-].*/) ? drawYear + parseInt(value.substring(1), 10) : + (value.match(/[+\-].*/) ? thisYear + parseInt(value, 10) : + parseInt(value, 10))); + return (isNaN(year) ? thisYear : year); + }; + year = determineYear(years[0]); + endYear = Math.max(year, determineYear(years[1] || "")); + year = (minDate ? Math.max(year, minDate.getFullYear()) : year); + endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); + inst.yearshtml += "<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>"; + for (; year <= endYear; year++) { + inst.yearshtml += "<option value='" + year + "'" + + (year === drawYear ? " selected='selected'" : "") + + ">" + year + "</option>"; + } + inst.yearshtml += "</select>"; + + html += inst.yearshtml; + inst.yearshtml = null; + } + } + + html += this._get(inst, "yearSuffix"); + if (showMonthAfterYear) { + html += (secondary || !(changeMonth && changeYear) ? " " : "") + monthHtml; + } + html += "</div>"; // Close datepicker_header + return html; + }, + + /* Adjust one of the date sub-fields. */ + _adjustInstDate: function(inst, offset, period) { + var year = inst.drawYear + (period === "Y" ? offset : 0), + month = inst.drawMonth + (period === "M" ? offset : 0), + day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === "D" ? offset : 0), + date = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day))); + + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + if (period === "M" || period === "Y") { + this._notifyChange(inst); + } + }, + + /* Ensure a date is within any min/max bounds. */ + _restrictMinMax: function(inst, date) { + var minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + newDate = (minDate && date < minDate ? minDate : date); + return (maxDate && newDate > maxDate ? maxDate : newDate); + }, + + /* Notify change of month/year. */ + _notifyChange: function(inst) { + var onChange = this._get(inst, "onChangeMonthYear"); + if (onChange) { + onChange.apply((inst.input ? inst.input[0] : null), + [inst.selectedYear, inst.selectedMonth + 1, inst]); + } + }, + + /* Determine the number of months to show. */ + _getNumberOfMonths: function(inst) { + var numMonths = this._get(inst, "numberOfMonths"); + return (numMonths == null ? [1, 1] : (typeof numMonths === "number" ? [1, numMonths] : numMonths)); + }, + + /* Determine the current maximum date - ensure no time components are set. */ + _getMinMaxDate: function(inst, minMax) { + return this._determineDate(inst, this._get(inst, minMax + "Date"), null); + }, + + /* Find the number of days in a given month. */ + _getDaysInMonth: function(year, month) { + return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); + }, + + /* Find the day of the week of the first of a month. */ + _getFirstDayOfMonth: function(year, month) { + return new Date(year, month, 1).getDay(); + }, + + /* Determines if we should allow a "next/prev" month display change. */ + _canAdjustMonth: function(inst, offset, curYear, curMonth) { + var numMonths = this._getNumberOfMonths(inst), + date = this._daylightSavingAdjust(new Date(curYear, + curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); + + if (offset < 0) { + date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); + } + return this._isInRange(inst, date); + }, + + /* Is the given date in the accepted range? */ + _isInRange: function(inst, date) { + var yearSplit, currentYear, + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + minYear = null, + maxYear = null, + years = this._get(inst, "yearRange"); + if (years){ + yearSplit = years.split(":"); + currentYear = new Date().getFullYear(); + minYear = parseInt(yearSplit[0], 10); + maxYear = parseInt(yearSplit[1], 10); + if ( yearSplit[0].match(/[+\-].*/) ) { + minYear += currentYear; + } + if ( yearSplit[1].match(/[+\-].*/) ) { + maxYear += currentYear; + } + } + + return ((!minDate || date.getTime() >= minDate.getTime()) && + (!maxDate || date.getTime() <= maxDate.getTime()) && + (!minYear || date.getFullYear() >= minYear) && + (!maxYear || date.getFullYear() <= maxYear)); + }, + + /* Provide the configuration settings for formatting/parsing. */ + _getFormatConfig: function(inst) { + var shortYearCutoff = this._get(inst, "shortYearCutoff"); + shortYearCutoff = (typeof shortYearCutoff !== "string" ? shortYearCutoff : + new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); + return {shortYearCutoff: shortYearCutoff, + dayNamesShort: this._get(inst, "dayNamesShort"), dayNames: this._get(inst, "dayNames"), + monthNamesShort: this._get(inst, "monthNamesShort"), monthNames: this._get(inst, "monthNames")}; + }, + + /* Format the given date for display. */ + _formatDate: function(inst, day, month, year) { + if (!day) { + inst.currentDay = inst.selectedDay; + inst.currentMonth = inst.selectedMonth; + inst.currentYear = inst.selectedYear; + } + var date = (day ? (typeof day === "object" ? day : + this._daylightSavingAdjust(new Date(year, month, day))) : + this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); + return this.formatDate(this._get(inst, "dateFormat"), date, this._getFormatConfig(inst)); + } +}); + +/* + * Bind hover events for datepicker elements. + * Done via delegate so the binding only occurs once in the lifetime of the parent div. + * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. + */ +function bindHover(dpDiv) { + var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a"; + return dpDiv.delegate(selector, "mouseout", function() { + $(this).removeClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).removeClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).removeClass("ui-datepicker-next-hover"); + } + }) + .delegate(selector, "mouseover", function(){ + if (!$.datepicker._isDisabledDatepicker( instActive.inline ? dpDiv.parent()[0] : instActive.input[0])) { + $(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); + $(this).addClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).addClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).addClass("ui-datepicker-next-hover"); + } + } + }); +} + +/* jQuery extend now ignores nulls! */ +function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] == null) { + target[name] = props[name]; + } + } + return target; +} + +/* Invoke the datepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new datepicker functionality + @return jQuery object */ +$.fn.datepicker = function(options){ + + /* Verify an empty collection wasn't passed - Fixes #6976 */ + if ( !this.length ) { + return this; + } + + /* Initialise the date picker. */ + if (!$.datepicker.initialized) { + $(document).mousedown($.datepicker._checkExternalClick); + $.datepicker.initialized = true; + } + + /* Append datepicker main container to body if not exist. */ + if ($("#"+$.datepicker._mainDivId).length === 0) { + $("body").append($.datepicker.dpDiv); + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options === "string" && (options === "isDisabled" || options === "getDate" || options === "widget")) { + return $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this[0]].concat(otherArgs)); + } + if (options === "option" && arguments.length === 2 && typeof arguments[1] === "string") { + return $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this[0]].concat(otherArgs)); + } + return this.each(function() { + typeof options === "string" ? + $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this].concat(otherArgs)) : + $.datepicker._attachDatepicker(this, options); + }); +}; + +$.datepicker = new Datepicker(); // singleton instance +$.datepicker.initialized = false; +$.datepicker.uuid = new Date().getTime(); +$.datepicker.version = "1.10.2"; + +// Workaround for #4055 +// Add another global to avoid noConflict issues with inline event handlers +window["DP_jQuery_" + dpuuid] = $; + +})(jQuery); diff --git a/dist/jquery-editable/jquery-ui-datepicker/js/jquery-ui-1.10.2.custom.min.js b/dist/jquery-editable/jquery-ui-datepicker/js/jquery-ui-1.10.2.custom.min.js new file mode 100644 index 0000000..443ca0a --- /dev/null +++ b/dist/jquery-editable/jquery-ui-datepicker/js/jquery-ui-1.10.2.custom.min.js @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.10.2 - 2013-04-07 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.datepicker.js +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ + +(function(e,t){function i(t,i){var a,n,r,o=t.nodeName.toLowerCase();return"area"===o?(a=t.parentNode,n=a.name,t.href&&n&&"map"===a.nodeName.toLowerCase()?(r=e("img[usemap=#"+n+"]")[0],!!r&&s(r)):!1):(/input|select|textarea|button|object/.test(o)?!t.disabled:"a"===o?t.href||i:i)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var a=0,n=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.2",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,s){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),s&&s.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var s,a,n=e(this[0]);n.length&&n[0]!==document;){if(s=n.css("position"),("absolute"===s||"relative"===s||"fixed"===s)&&(a=parseInt(n.css("zIndex"),10),!isNaN(a)&&0!==a))return a;n=n.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++a)})},removeUniqueId:function(){return this.each(function(){n.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,s){return!!e.data(t,s[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var s=e.attr(t,"tabindex"),a=isNaN(s);return(a||s>=0)&&i(t,!a)}}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(i,s){function a(t,i,s,a){return e.each(n,function(){i-=parseFloat(e.css(t,"padding"+this))||0,s&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),a&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var n="Width"===s?["Left","Right"]:["Top","Bottom"],r=s.toLowerCase(),o={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+s]=function(i){return i===t?o["inner"+s].call(this):this.each(function(){e(this).css(r,a(this,i)+"px")})},e.fn["outer"+s]=function(t,i){return"number"!=typeof t?o["outer"+s].call(this,t):this.each(function(){e(this).css(r,a(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,s){var a,n=e.ui[t].prototype;for(a in s)n.plugins[a]=n.plugins[a]||[],n.plugins[a].push([i,s[a]])},call:function(e,t,i){var s,a=e.plugins[t];if(a&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(s=0;a.length>s;s++)e.options[a[s][0]]&&a[s][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",a=!1;return t[s]>0?!0:(t[s]=1,a=t[s]>0,t[s]=0,a)}})})(jQuery);(function(t,e){function i(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},t.extend(this._defaults,this.regional[""]),this.dpDiv=s(t("<div id='"+this._mainDivId+"' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"))}function s(e){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.delegate(i,"mouseout",function(){t(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).removeClass("ui-datepicker-next-hover")}).delegate(i,"mouseover",function(){t.datepicker._isDisabledDatepicker(a.inline?e.parent()[0]:a.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).addClass("ui-datepicker-next-hover"))})}function n(e,i){t.extend(e,i);for(var s in i)null==i[s]&&(e[s]=i[s]);return e}t.extend(t.ui,{datepicker:{version:"1.10.2"}});var a,r="datepicker",o=(new Date).getTime();t.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(t){return n(this._defaults,t||{}),this},_attachDatepicker:function(e,i){var s,n,a;s=e.nodeName.toLowerCase(),n="div"===s||"span"===s,e.id||(this.uuid+=1,e.id="dp"+this.uuid),a=this._newInst(t(e),n),a.settings=t.extend({},i||{}),"input"===s?this._connectDatepicker(e,a):n&&this._inlineDatepicker(e,a)},_newInst:function(e,i){var n=e[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:n,input:e,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?s(t("<div class='"+this._inlineClass+" ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")):this.dpDiv}},_connectDatepicker:function(e,i){var s=t(e);i.append=t([]),i.trigger=t([]),s.hasClass(this.markerClassName)||(this._attachments(s,i),s.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp),this._autoSize(i),t.data(e,r,i),i.settings.disabled&&this._disableDatepicker(e))},_attachments:function(e,i){var s,n,a,r=this._get(i,"appendText"),o=this._get(i,"isRTL");i.append&&i.append.remove(),r&&(i.append=t("<span class='"+this._appendClass+"'>"+r+"</span>"),e[o?"before":"after"](i.append)),e.unbind("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),s=this._get(i,"showOn"),("focus"===s||"both"===s)&&e.focus(this._showDatepicker),("button"===s||"both"===s)&&(n=this._get(i,"buttonText"),a=this._get(i,"buttonImage"),i.trigger=t(this._get(i,"buttonImageOnly")?t("<img/>").addClass(this._triggerClass).attr({src:a,alt:n,title:n}):t("<button type='button'></button>").addClass(this._triggerClass).html(a?t("<img/>").attr({src:a,alt:n,title:n}):n)),e[o?"before":"after"](i.trigger),i.trigger.click(function(){return t.datepicker._datepickerShowing&&t.datepicker._lastInput===e[0]?t.datepicker._hideDatepicker():t.datepicker._datepickerShowing&&t.datepicker._lastInput!==e[0]?(t.datepicker._hideDatepicker(),t.datepicker._showDatepicker(e[0])):t.datepicker._showDatepicker(e[0]),!1}))},_autoSize:function(t){if(this._get(t,"autoSize")&&!t.inline){var e,i,s,n,a=new Date(2009,11,20),r=this._get(t,"dateFormat");r.match(/[DM]/)&&(e=function(t){for(i=0,s=0,n=0;t.length>n;n++)t[n].length>i&&(i=t[n].length,s=n);return s},a.setMonth(e(this._get(t,r.match(/MM/)?"monthNames":"monthNamesShort"))),a.setDate(e(this._get(t,r.match(/DD/)?"dayNames":"dayNamesShort"))+20-a.getDay())),t.input.attr("size",this._formatDate(t,a).length)}},_inlineDatepicker:function(e,i){var s=t(e);s.hasClass(this.markerClassName)||(s.addClass(this.markerClassName).append(i.dpDiv),t.data(e,r,i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(e),i.dpDiv.css("display","block"))},_dialogDatepicker:function(e,i,s,a,o){var h,l,c,u,d,p=this._dialogInst;return p||(this.uuid+=1,h="dp"+this.uuid,this._dialogInput=t("<input type='text' id='"+h+"' style='position: absolute; top: -100px; width: 0px;'/>"),this._dialogInput.keydown(this._doKeyDown),t("body").append(this._dialogInput),p=this._dialogInst=this._newInst(this._dialogInput,!1),p.settings={},t.data(this._dialogInput[0],r,p)),n(p.settings,a||{}),i=i&&i.constructor===Date?this._formatDate(p,i):i,this._dialogInput.val(i),this._pos=o?o.length?o:[o.pageX,o.pageY]:null,this._pos||(l=document.documentElement.clientWidth,c=document.documentElement.clientHeight,u=document.documentElement.scrollLeft||document.body.scrollLeft,d=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[l/2-100+u,c/2-150+d]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),p.settings.onSelect=s,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),t.blockUI&&t.blockUI(this.dpDiv),t.data(this._dialogInput[0],r,p),this},_destroyDatepicker:function(e){var i,s=t(e),n=t.data(e,r);s.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),t.removeData(e,r),"input"===i?(n.append.remove(),n.trigger.remove(),s.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):("div"===i||"span"===i)&&s.removeClass(this.markerClassName).empty())},_enableDatepicker:function(e){var i,s,n=t(e),a=t.data(e,r);n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!1,a.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().removeClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}))},_disableDatepicker:function(e){var i,s,n=t(e),a=t.data(e,r);n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!0,a.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().addClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}),this._disabledInputs[this._disabledInputs.length]=e)},_isDisabledDatepicker:function(t){if(!t)return!1;for(var e=0;this._disabledInputs.length>e;e++)if(this._disabledInputs[e]===t)return!0;return!1},_getInst:function(e){try{return t.data(e,r)}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(i,s,a){var r,o,h,l,c=this._getInst(i);return 2===arguments.length&&"string"==typeof s?"defaults"===s?t.extend({},t.datepicker._defaults):c?"all"===s?t.extend({},c.settings):this._get(c,s):null:(r=s||{},"string"==typeof s&&(r={},r[s]=a),c&&(this._curInst===c&&this._hideDatepicker(),o=this._getDateDatepicker(i,!0),h=this._getMinMaxDate(c,"min"),l=this._getMinMaxDate(c,"max"),n(c.settings,r),null!==h&&r.dateFormat!==e&&r.minDate===e&&(c.settings.minDate=this._formatDate(c,h)),null!==l&&r.dateFormat!==e&&r.maxDate===e&&(c.settings.maxDate=this._formatDate(c,l)),"disabled"in r&&(r.disabled?this._disableDatepicker(i):this._enableDatepicker(i)),this._attachments(t(i),c),this._autoSize(c),this._setDate(c,o),this._updateAlternate(c),this._updateDatepicker(c)),e)},_changeDatepicker:function(t,e,i){this._optionDatepicker(t,e,i)},_refreshDatepicker:function(t){var e=this._getInst(t);e&&this._updateDatepicker(e)},_setDateDatepicker:function(t,e){var i=this._getInst(t);i&&(this._setDate(i,e),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(t,e){var i=this._getInst(t);return i&&!i.inline&&this._setDateFromField(i,e),i?this._getDate(i):null},_doKeyDown:function(e){var i,s,n,a=t.datepicker._getInst(e.target),r=!0,o=a.dpDiv.is(".ui-datepicker-rtl");if(a._keyEvent=!0,t.datepicker._datepickerShowing)switch(e.keyCode){case 9:t.datepicker._hideDatepicker(),r=!1;break;case 13:return n=t("td."+t.datepicker._dayOverClass+":not(."+t.datepicker._currentClass+")",a.dpDiv),n[0]&&t.datepicker._selectDay(e.target,a.selectedMonth,a.selectedYear,n[0]),i=t.datepicker._get(a,"onSelect"),i?(s=t.datepicker._formatDate(a),i.apply(a.input?a.input[0]:null,[s,a])):t.datepicker._hideDatepicker(),!1;case 27:t.datepicker._hideDatepicker();break;case 33:t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(a,"stepBigMonths"):-t.datepicker._get(a,"stepMonths"),"M");break;case 34:t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(a,"stepBigMonths"):+t.datepicker._get(a,"stepMonths"),"M");break;case 35:(e.ctrlKey||e.metaKey)&&t.datepicker._clearDate(e.target),r=e.ctrlKey||e.metaKey;break;case 36:(e.ctrlKey||e.metaKey)&&t.datepicker._gotoToday(e.target),r=e.ctrlKey||e.metaKey;break;case 37:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,o?1:-1,"D"),r=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(a,"stepBigMonths"):-t.datepicker._get(a,"stepMonths"),"M");break;case 38:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,-7,"D"),r=e.ctrlKey||e.metaKey;break;case 39:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,o?-1:1,"D"),r=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(a,"stepBigMonths"):+t.datepicker._get(a,"stepMonths"),"M");break;case 40:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,7,"D"),r=e.ctrlKey||e.metaKey;break;default:r=!1}else 36===e.keyCode&&e.ctrlKey?t.datepicker._showDatepicker(this):r=!1;r&&(e.preventDefault(),e.stopPropagation())},_doKeyPress:function(i){var s,n,a=t.datepicker._getInst(i.target);return t.datepicker._get(a,"constrainInput")?(s=t.datepicker._possibleChars(t.datepicker._get(a,"dateFormat")),n=String.fromCharCode(null==i.charCode?i.keyCode:i.charCode),i.ctrlKey||i.metaKey||" ">n||!s||s.indexOf(n)>-1):e},_doKeyUp:function(e){var i,s=t.datepicker._getInst(e.target);if(s.input.val()!==s.lastVal)try{i=t.datepicker.parseDate(t.datepicker._get(s,"dateFormat"),s.input?s.input.val():null,t.datepicker._getFormatConfig(s)),i&&(t.datepicker._setDateFromField(s),t.datepicker._updateAlternate(s),t.datepicker._updateDatepicker(s))}catch(n){}return!0},_showDatepicker:function(e){if(e=e.target||e,"input"!==e.nodeName.toLowerCase()&&(e=t("input",e.parentNode)[0]),!t.datepicker._isDisabledDatepicker(e)&&t.datepicker._lastInput!==e){var i,s,a,r,o,h,l;i=t.datepicker._getInst(e),t.datepicker._curInst&&t.datepicker._curInst!==i&&(t.datepicker._curInst.dpDiv.stop(!0,!0),i&&t.datepicker._datepickerShowing&&t.datepicker._hideDatepicker(t.datepicker._curInst.input[0])),s=t.datepicker._get(i,"beforeShow"),a=s?s.apply(e,[e,i]):{},a!==!1&&(n(i.settings,a),i.lastVal=null,t.datepicker._lastInput=e,t.datepicker._setDateFromField(i),t.datepicker._inDialog&&(e.value=""),t.datepicker._pos||(t.datepicker._pos=t.datepicker._findPos(e),t.datepicker._pos[1]+=e.offsetHeight),r=!1,t(e).parents().each(function(){return r|="fixed"===t(this).css("position"),!r}),o={left:t.datepicker._pos[0],top:t.datepicker._pos[1]},t.datepicker._pos=null,i.dpDiv.empty(),i.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),t.datepicker._updateDatepicker(i),o=t.datepicker._checkOffset(i,o,r),i.dpDiv.css({position:t.datepicker._inDialog&&t.blockUI?"static":r?"fixed":"absolute",display:"none",left:o.left+"px",top:o.top+"px"}),i.inline||(h=t.datepicker._get(i,"showAnim"),l=t.datepicker._get(i,"duration"),i.dpDiv.zIndex(t(e).zIndex()+1),t.datepicker._datepickerShowing=!0,t.effects&&t.effects.effect[h]?i.dpDiv.show(h,t.datepicker._get(i,"showOptions"),l):i.dpDiv[h||"show"](h?l:null),i.input.is(":visible")&&!i.input.is(":disabled")&&i.input.focus(),t.datepicker._curInst=i))}},_updateDatepicker:function(e){this.maxRows=4,a=e,e.dpDiv.empty().append(this._generateHTML(e)),this._attachHandlers(e),e.dpDiv.find("."+this._dayOverClass+" a").mouseover();var i,s=this._getNumberOfMonths(e),n=s[1],r=17;e.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),n>1&&e.dpDiv.addClass("ui-datepicker-multi-"+n).css("width",r*n+"em"),e.dpDiv[(1!==s[0]||1!==s[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),e.dpDiv[(this._get(e,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),e===t.datepicker._curInst&&t.datepicker._datepickerShowing&&e.input&&e.input.is(":visible")&&!e.input.is(":disabled")&&e.input[0]!==document.activeElement&&e.input.focus(),e.yearshtml&&(i=e.yearshtml,setTimeout(function(){i===e.yearshtml&&e.yearshtml&&e.dpDiv.find("select.ui-datepicker-year:first").replaceWith(e.yearshtml),i=e.yearshtml=null},0))},_getBorders:function(t){var e=function(t){return{thin:1,medium:2,thick:3}[t]||t};return[parseFloat(e(t.css("border-left-width"))),parseFloat(e(t.css("border-top-width")))]},_checkOffset:function(e,i,s){var n=e.dpDiv.outerWidth(),a=e.dpDiv.outerHeight(),r=e.input?e.input.outerWidth():0,o=e.input?e.input.outerHeight():0,h=document.documentElement.clientWidth+(s?0:t(document).scrollLeft()),l=document.documentElement.clientHeight+(s?0:t(document).scrollTop());return i.left-=this._get(e,"isRTL")?n-r:0,i.left-=s&&i.left===e.input.offset().left?t(document).scrollLeft():0,i.top-=s&&i.top===e.input.offset().top+o?t(document).scrollTop():0,i.left-=Math.min(i.left,i.left+n>h&&h>n?Math.abs(i.left+n-h):0),i.top-=Math.min(i.top,i.top+a>l&&l>a?Math.abs(a+o):0),i},_findPos:function(e){for(var i,s=this._getInst(e),n=this._get(s,"isRTL");e&&("hidden"===e.type||1!==e.nodeType||t.expr.filters.hidden(e));)e=e[n?"previousSibling":"nextSibling"];return i=t(e).offset(),[i.left,i.top]},_hideDatepicker:function(e){var i,s,n,a,o=this._curInst;!o||e&&o!==t.data(e,r)||this._datepickerShowing&&(i=this._get(o,"showAnim"),s=this._get(o,"duration"),n=function(){t.datepicker._tidyDialog(o)},t.effects&&(t.effects.effect[i]||t.effects[i])?o.dpDiv.hide(i,t.datepicker._get(o,"showOptions"),s,n):o.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?s:null,n),i||n(),this._datepickerShowing=!1,a=this._get(o,"onClose"),a&&a.apply(o.input?o.input[0]:null,[o.input?o.input.val():"",o]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),t.blockUI&&(t.unblockUI(),t("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(t){t.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(e){if(t.datepicker._curInst){var i=t(e.target),s=t.datepicker._getInst(i[0]);(i[0].id!==t.datepicker._mainDivId&&0===i.parents("#"+t.datepicker._mainDivId).length&&!i.hasClass(t.datepicker.markerClassName)&&!i.closest("."+t.datepicker._triggerClass).length&&t.datepicker._datepickerShowing&&(!t.datepicker._inDialog||!t.blockUI)||i.hasClass(t.datepicker.markerClassName)&&t.datepicker._curInst!==s)&&t.datepicker._hideDatepicker()}},_adjustDate:function(e,i,s){var n=t(e),a=this._getInst(n[0]);this._isDisabledDatepicker(n[0])||(this._adjustInstDate(a,i+("M"===s?this._get(a,"showCurrentAtPos"):0),s),this._updateDatepicker(a))},_gotoToday:function(e){var i,s=t(e),n=this._getInst(s[0]);this._get(n,"gotoCurrent")&&n.currentDay?(n.selectedDay=n.currentDay,n.drawMonth=n.selectedMonth=n.currentMonth,n.drawYear=n.selectedYear=n.currentYear):(i=new Date,n.selectedDay=i.getDate(),n.drawMonth=n.selectedMonth=i.getMonth(),n.drawYear=n.selectedYear=i.getFullYear()),this._notifyChange(n),this._adjustDate(s)},_selectMonthYear:function(e,i,s){var n=t(e),a=this._getInst(n[0]);a["selected"+("M"===s?"Month":"Year")]=a["draw"+("M"===s?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(a),this._adjustDate(n)},_selectDay:function(e,i,s,n){var a,r=t(e);t(n).hasClass(this._unselectableClass)||this._isDisabledDatepicker(r[0])||(a=this._getInst(r[0]),a.selectedDay=a.currentDay=t("a",n).html(),a.selectedMonth=a.currentMonth=i,a.selectedYear=a.currentYear=s,this._selectDate(e,this._formatDate(a,a.currentDay,a.currentMonth,a.currentYear)))},_clearDate:function(e){var i=t(e);this._selectDate(i,"")},_selectDate:function(e,i){var s,n=t(e),a=this._getInst(n[0]);i=null!=i?i:this._formatDate(a),a.input&&a.input.val(i),this._updateAlternate(a),s=this._get(a,"onSelect"),s?s.apply(a.input?a.input[0]:null,[i,a]):a.input&&a.input.trigger("change"),a.inline?this._updateDatepicker(a):(this._hideDatepicker(),this._lastInput=a.input[0],"object"!=typeof a.input[0]&&a.input.focus(),this._lastInput=null)},_updateAlternate:function(e){var i,s,n,a=this._get(e,"altField");a&&(i=this._get(e,"altFormat")||this._get(e,"dateFormat"),s=this._getDate(e),n=this.formatDate(i,s,this._getFormatConfig(e)),t(a).each(function(){t(this).val(n)}))},noWeekends:function(t){var e=t.getDay();return[e>0&&6>e,""]},iso8601Week:function(t){var e,i=new Date(t.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),e=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((e-i)/864e5)/7)+1},parseDate:function(i,s,n){if(null==i||null==s)throw"Invalid arguments";if(s="object"==typeof s?""+s:s+"",""===s)return null;var a,r,o,h,l=0,c=(n?n.shortYearCutoff:null)||this._defaults.shortYearCutoff,u="string"!=typeof c?c:(new Date).getFullYear()%100+parseInt(c,10),d=(n?n.dayNamesShort:null)||this._defaults.dayNamesShort,p=(n?n.dayNames:null)||this._defaults.dayNames,f=(n?n.monthNamesShort:null)||this._defaults.monthNamesShort,m=(n?n.monthNames:null)||this._defaults.monthNames,g=-1,v=-1,_=-1,b=-1,y=!1,w=function(t){var e=i.length>a+1&&i.charAt(a+1)===t;return e&&a++,e},k=function(t){var e=w(t),i="@"===t?14:"!"===t?20:"y"===t&&e?4:"o"===t?3:2,n=RegExp("^\\d{1,"+i+"}"),a=s.substring(l).match(n);if(!a)throw"Missing number at position "+l;return l+=a[0].length,parseInt(a[0],10)},x=function(i,n,a){var r=-1,o=t.map(w(i)?a:n,function(t,e){return[[e,t]]}).sort(function(t,e){return-(t[1].length-e[1].length)});if(t.each(o,function(t,i){var n=i[1];return s.substr(l,n.length).toLowerCase()===n.toLowerCase()?(r=i[0],l+=n.length,!1):e}),-1!==r)return r+1;throw"Unknown name at position "+l},D=function(){if(s.charAt(l)!==i.charAt(a))throw"Unexpected literal at position "+l;l++};for(a=0;i.length>a;a++)if(y)"'"!==i.charAt(a)||w("'")?D():y=!1;else switch(i.charAt(a)){case"d":_=k("d");break;case"D":x("D",d,p);break;case"o":b=k("o");break;case"m":v=k("m");break;case"M":v=x("M",f,m);break;case"y":g=k("y");break;case"@":h=new Date(k("@")),g=h.getFullYear(),v=h.getMonth()+1,_=h.getDate();break;case"!":h=new Date((k("!")-this._ticksTo1970)/1e4),g=h.getFullYear(),v=h.getMonth()+1,_=h.getDate();break;case"'":w("'")?D():y=!0;break;default:D()}if(s.length>l&&(o=s.substr(l),!/^\s+/.test(o)))throw"Extra/unparsed characters found in date: "+o;if(-1===g?g=(new Date).getFullYear():100>g&&(g+=(new Date).getFullYear()-(new Date).getFullYear()%100+(u>=g?0:-100)),b>-1)for(v=1,_=b;;){if(r=this._getDaysInMonth(g,v-1),r>=_)break;v++,_-=r}if(h=this._daylightSavingAdjust(new Date(g,v-1,_)),h.getFullYear()!==g||h.getMonth()+1!==v||h.getDate()!==_)throw"Invalid date";return h},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(t,e,i){if(!e)return"";var s,n=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,a=(i?i.dayNames:null)||this._defaults.dayNames,r=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,o=(i?i.monthNames:null)||this._defaults.monthNames,h=function(e){var i=t.length>s+1&&t.charAt(s+1)===e;return i&&s++,i},l=function(t,e,i){var s=""+e;if(h(t))for(;i>s.length;)s="0"+s;return s},c=function(t,e,i,s){return h(t)?s[e]:i[e]},u="",d=!1;if(e)for(s=0;t.length>s;s++)if(d)"'"!==t.charAt(s)||h("'")?u+=t.charAt(s):d=!1;else switch(t.charAt(s)){case"d":u+=l("d",e.getDate(),2);break;case"D":u+=c("D",e.getDay(),n,a);break;case"o":u+=l("o",Math.round((new Date(e.getFullYear(),e.getMonth(),e.getDate()).getTime()-new Date(e.getFullYear(),0,0).getTime())/864e5),3);break;case"m":u+=l("m",e.getMonth()+1,2);break;case"M":u+=c("M",e.getMonth(),r,o);break;case"y":u+=h("y")?e.getFullYear():(10>e.getYear()%100?"0":"")+e.getYear()%100;break;case"@":u+=e.getTime();break;case"!":u+=1e4*e.getTime()+this._ticksTo1970;break;case"'":h("'")?u+="'":d=!0;break;default:u+=t.charAt(s)}return u},_possibleChars:function(t){var e,i="",s=!1,n=function(i){var s=t.length>e+1&&t.charAt(e+1)===i;return s&&e++,s};for(e=0;t.length>e;e++)if(s)"'"!==t.charAt(e)||n("'")?i+=t.charAt(e):s=!1;else switch(t.charAt(e)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":n("'")?i+="'":s=!0;break;default:i+=t.charAt(e)}return i},_get:function(t,i){return t.settings[i]!==e?t.settings[i]:this._defaults[i]},_setDateFromField:function(t,e){if(t.input.val()!==t.lastVal){var i=this._get(t,"dateFormat"),s=t.lastVal=t.input?t.input.val():null,n=this._getDefaultDate(t),a=n,r=this._getFormatConfig(t);try{a=this.parseDate(i,s,r)||n}catch(o){s=e?"":s}t.selectedDay=a.getDate(),t.drawMonth=t.selectedMonth=a.getMonth(),t.drawYear=t.selectedYear=a.getFullYear(),t.currentDay=s?a.getDate():0,t.currentMonth=s?a.getMonth():0,t.currentYear=s?a.getFullYear():0,this._adjustInstDate(t)}},_getDefaultDate:function(t){return this._restrictMinMax(t,this._determineDate(t,this._get(t,"defaultDate"),new Date))},_determineDate:function(e,i,s){var n=function(t){var e=new Date;return e.setDate(e.getDate()+t),e},a=function(i){try{return t.datepicker.parseDate(t.datepicker._get(e,"dateFormat"),i,t.datepicker._getFormatConfig(e))}catch(s){}for(var n=(i.toLowerCase().match(/^c/)?t.datepicker._getDate(e):null)||new Date,a=n.getFullYear(),r=n.getMonth(),o=n.getDate(),h=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,l=h.exec(i);l;){switch(l[2]||"d"){case"d":case"D":o+=parseInt(l[1],10);break;case"w":case"W":o+=7*parseInt(l[1],10);break;case"m":case"M":r+=parseInt(l[1],10),o=Math.min(o,t.datepicker._getDaysInMonth(a,r));break;case"y":case"Y":a+=parseInt(l[1],10),o=Math.min(o,t.datepicker._getDaysInMonth(a,r))}l=h.exec(i)}return new Date(a,r,o)},r=null==i||""===i?s:"string"==typeof i?a(i):"number"==typeof i?isNaN(i)?s:n(i):new Date(i.getTime());return r=r&&"Invalid Date"==""+r?s:r,r&&(r.setHours(0),r.setMinutes(0),r.setSeconds(0),r.setMilliseconds(0)),this._daylightSavingAdjust(r)},_daylightSavingAdjust:function(t){return t?(t.setHours(t.getHours()>12?t.getHours()+2:0),t):null},_setDate:function(t,e,i){var s=!e,n=t.selectedMonth,a=t.selectedYear,r=this._restrictMinMax(t,this._determineDate(t,e,new Date));t.selectedDay=t.currentDay=r.getDate(),t.drawMonth=t.selectedMonth=t.currentMonth=r.getMonth(),t.drawYear=t.selectedYear=t.currentYear=r.getFullYear(),n===t.selectedMonth&&a===t.selectedYear||i||this._notifyChange(t),this._adjustInstDate(t),t.input&&t.input.val(s?"":this._formatDate(t))},_getDate:function(t){var e=!t.currentYear||t.input&&""===t.input.val()?null:this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return e},_attachHandlers:function(e){var i=this._get(e,"stepMonths"),s="#"+e.id.replace(/\\\\/g,"\\");e.dpDiv.find("[data-handler]").map(function(){var e={prev:function(){window["DP_jQuery_"+o].datepicker._adjustDate(s,-i,"M")},next:function(){window["DP_jQuery_"+o].datepicker._adjustDate(s,+i,"M")},hide:function(){window["DP_jQuery_"+o].datepicker._hideDatepicker()},today:function(){window["DP_jQuery_"+o].datepicker._gotoToday(s)},selectDay:function(){return window["DP_jQuery_"+o].datepicker._selectDay(s,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return window["DP_jQuery_"+o].datepicker._selectMonthYear(s,this,"M"),!1},selectYear:function(){return window["DP_jQuery_"+o].datepicker._selectMonthYear(s,this,"Y"),!1}};t(this).bind(this.getAttribute("data-event"),e[this.getAttribute("data-handler")])})},_generateHTML:function(t){var e,i,s,n,a,r,o,h,l,c,u,d,p,f,m,g,v,_,b,y,w,k,x,D,T,C,S,M,N,I,P,A,z,H,E,F,O,W,j,R=new Date,L=this._daylightSavingAdjust(new Date(R.getFullYear(),R.getMonth(),R.getDate())),Y=this._get(t,"isRTL"),B=this._get(t,"showButtonPanel"),J=this._get(t,"hideIfNoPrevNext"),Q=this._get(t,"navigationAsDateFormat"),K=this._getNumberOfMonths(t),V=this._get(t,"showCurrentAtPos"),U=this._get(t,"stepMonths"),q=1!==K[0]||1!==K[1],X=this._daylightSavingAdjust(t.currentDay?new Date(t.currentYear,t.currentMonth,t.currentDay):new Date(9999,9,9)),G=this._getMinMaxDate(t,"min"),$=this._getMinMaxDate(t,"max"),Z=t.drawMonth-V,te=t.drawYear;if(0>Z&&(Z+=12,te--),$)for(e=this._daylightSavingAdjust(new Date($.getFullYear(),$.getMonth()-K[0]*K[1]+1,$.getDate())),e=G&&G>e?G:e;this._daylightSavingAdjust(new Date(te,Z,1))>e;)Z--,0>Z&&(Z=11,te--);for(t.drawMonth=Z,t.drawYear=te,i=this._get(t,"prevText"),i=Q?this.formatDate(i,this._daylightSavingAdjust(new Date(te,Z-U,1)),this._getFormatConfig(t)):i,s=this._canAdjustMonth(t,-1,te,Z)?"<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(Y?"e":"w")+"'>"+i+"</span></a>":J?"":"<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(Y?"e":"w")+"'>"+i+"</span></a>",n=this._get(t,"nextText"),n=Q?this.formatDate(n,this._daylightSavingAdjust(new Date(te,Z+U,1)),this._getFormatConfig(t)):n,a=this._canAdjustMonth(t,1,te,Z)?"<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click' title='"+n+"'><span class='ui-icon ui-icon-circle-triangle-"+(Y?"w":"e")+"'>"+n+"</span></a>":J?"":"<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+n+"'><span class='ui-icon ui-icon-circle-triangle-"+(Y?"w":"e")+"'>"+n+"</span></a>",r=this._get(t,"currentText"),o=this._get(t,"gotoCurrent")&&t.currentDay?X:L,r=Q?this.formatDate(r,o,this._getFormatConfig(t)):r,h=t.inline?"":"<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>"+this._get(t,"closeText")+"</button>",l=B?"<div class='ui-datepicker-buttonpane ui-widget-content'>"+(Y?h:"")+(this._isInRange(t,o)?"<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'>"+r+"</button>":"")+(Y?"":h)+"</div>":"",c=parseInt(this._get(t,"firstDay"),10),c=isNaN(c)?0:c,u=this._get(t,"showWeek"),d=this._get(t,"dayNames"),p=this._get(t,"dayNamesMin"),f=this._get(t,"monthNames"),m=this._get(t,"monthNamesShort"),g=this._get(t,"beforeShowDay"),v=this._get(t,"showOtherMonths"),_=this._get(t,"selectOtherMonths"),b=this._getDefaultDate(t),y="",k=0;K[0]>k;k++){for(x="",this.maxRows=4,D=0;K[1]>D;D++){if(T=this._daylightSavingAdjust(new Date(te,Z,t.selectedDay)),C=" ui-corner-all",S="",q){if(S+="<div class='ui-datepicker-group",K[1]>1)switch(D){case 0:S+=" ui-datepicker-group-first",C=" ui-corner-"+(Y?"right":"left");break;case K[1]-1:S+=" ui-datepicker-group-last",C=" ui-corner-"+(Y?"left":"right");break;default:S+=" ui-datepicker-group-middle",C=""}S+="'>"}for(S+="<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix"+C+"'>"+(/all|left/.test(C)&&0===k?Y?a:s:"")+(/all|right/.test(C)&&0===k?Y?s:a:"")+this._generateMonthYearHeader(t,Z,te,G,$,k>0||D>0,f,m)+"</div><table class='ui-datepicker-calendar'><thead>"+"<tr>",M=u?"<th class='ui-datepicker-week-col'>"+this._get(t,"weekHeader")+"</th>":"",w=0;7>w;w++)N=(w+c)%7,M+="<th"+((w+c+6)%7>=5?" class='ui-datepicker-week-end'":"")+">"+"<span title='"+d[N]+"'>"+p[N]+"</span></th>";for(S+=M+"</tr></thead><tbody>",I=this._getDaysInMonth(te,Z),te===t.selectedYear&&Z===t.selectedMonth&&(t.selectedDay=Math.min(t.selectedDay,I)),P=(this._getFirstDayOfMonth(te,Z)-c+7)%7,A=Math.ceil((P+I)/7),z=q?this.maxRows>A?this.maxRows:A:A,this.maxRows=z,H=this._daylightSavingAdjust(new Date(te,Z,1-P)),E=0;z>E;E++){for(S+="<tr>",F=u?"<td class='ui-datepicker-week-col'>"+this._get(t,"calculateWeek")(H)+"</td>":"",w=0;7>w;w++)O=g?g.apply(t.input?t.input[0]:null,[H]):[!0,""],W=H.getMonth()!==Z,j=W&&!_||!O[0]||G&&G>H||$&&H>$,F+="<td class='"+((w+c+6)%7>=5?" ui-datepicker-week-end":"")+(W?" ui-datepicker-other-month":"")+(H.getTime()===T.getTime()&&Z===t.selectedMonth&&t._keyEvent||b.getTime()===H.getTime()&&b.getTime()===T.getTime()?" "+this._dayOverClass:"")+(j?" "+this._unselectableClass+" ui-state-disabled":"")+(W&&!v?"":" "+O[1]+(H.getTime()===X.getTime()?" "+this._currentClass:"")+(H.getTime()===L.getTime()?" ui-datepicker-today":""))+"'"+(W&&!v||!O[2]?"":" title='"+O[2].replace(/'/g,"'")+"'")+(j?"":" data-handler='selectDay' data-event='click' data-month='"+H.getMonth()+"' data-year='"+H.getFullYear()+"'")+">"+(W&&!v?" ":j?"<span class='ui-state-default'>"+H.getDate()+"</span>":"<a class='ui-state-default"+(H.getTime()===L.getTime()?" ui-state-highlight":"")+(H.getTime()===X.getTime()?" ui-state-active":"")+(W?" ui-priority-secondary":"")+"' href='#'>"+H.getDate()+"</a>")+"</td>",H.setDate(H.getDate()+1),H=this._daylightSavingAdjust(H);S+=F+"</tr>"}Z++,Z>11&&(Z=0,te++),S+="</tbody></table>"+(q?"</div>"+(K[0]>0&&D===K[1]-1?"<div class='ui-datepicker-row-break'></div>":""):""),x+=S}y+=x}return y+=l,t._keyEvent=!1,y},_generateMonthYearHeader:function(t,e,i,s,n,a,r,o){var h,l,c,u,d,p,f,m,g=this._get(t,"changeMonth"),v=this._get(t,"changeYear"),_=this._get(t,"showMonthAfterYear"),b="<div class='ui-datepicker-title'>",y="";if(a||!g)y+="<span class='ui-datepicker-month'>"+r[e]+"</span>";else{for(h=s&&s.getFullYear()===i,l=n&&n.getFullYear()===i,y+="<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>",c=0;12>c;c++)(!h||c>=s.getMonth())&&(!l||n.getMonth()>=c)&&(y+="<option value='"+c+"'"+(c===e?" selected='selected'":"")+">"+o[c]+"</option>"); +y+="</select>"}if(_||(b+=y+(!a&&g&&v?"":" ")),!t.yearshtml)if(t.yearshtml="",a||!v)b+="<span class='ui-datepicker-year'>"+i+"</span>";else{for(u=this._get(t,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(t){var e=t.match(/c[+\-].*/)?i+parseInt(t.substring(1),10):t.match(/[+\-].*/)?d+parseInt(t,10):parseInt(t,10);return isNaN(e)?d:e},f=p(u[0]),m=Math.max(f,p(u[1]||"")),f=s?Math.max(f,s.getFullYear()):f,m=n?Math.min(m,n.getFullYear()):m,t.yearshtml+="<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>";m>=f;f++)t.yearshtml+="<option value='"+f+"'"+(f===i?" selected='selected'":"")+">"+f+"</option>";t.yearshtml+="</select>",b+=t.yearshtml,t.yearshtml=null}return b+=this._get(t,"yearSuffix"),_&&(b+=(!a&&g&&v?"":" ")+y),b+="</div>"},_adjustInstDate:function(t,e,i){var s=t.drawYear+("Y"===i?e:0),n=t.drawMonth+("M"===i?e:0),a=Math.min(t.selectedDay,this._getDaysInMonth(s,n))+("D"===i?e:0),r=this._restrictMinMax(t,this._daylightSavingAdjust(new Date(s,n,a)));t.selectedDay=r.getDate(),t.drawMonth=t.selectedMonth=r.getMonth(),t.drawYear=t.selectedYear=r.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(t)},_restrictMinMax:function(t,e){var i=this._getMinMaxDate(t,"min"),s=this._getMinMaxDate(t,"max"),n=i&&i>e?i:e;return s&&n>s?s:n},_notifyChange:function(t){var e=this._get(t,"onChangeMonthYear");e&&e.apply(t.input?t.input[0]:null,[t.selectedYear,t.selectedMonth+1,t])},_getNumberOfMonths:function(t){var e=this._get(t,"numberOfMonths");return null==e?[1,1]:"number"==typeof e?[1,e]:e},_getMinMaxDate:function(t,e){return this._determineDate(t,this._get(t,e+"Date"),null)},_getDaysInMonth:function(t,e){return 32-this._daylightSavingAdjust(new Date(t,e,32)).getDate()},_getFirstDayOfMonth:function(t,e){return new Date(t,e,1).getDay()},_canAdjustMonth:function(t,e,i,s){var n=this._getNumberOfMonths(t),a=this._daylightSavingAdjust(new Date(i,s+(0>e?e:n[0]*n[1]),1));return 0>e&&a.setDate(this._getDaysInMonth(a.getFullYear(),a.getMonth())),this._isInRange(t,a)},_isInRange:function(t,e){var i,s,n=this._getMinMaxDate(t,"min"),a=this._getMinMaxDate(t,"max"),r=null,o=null,h=this._get(t,"yearRange");return h&&(i=h.split(":"),s=(new Date).getFullYear(),r=parseInt(i[0],10),o=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(r+=s),i[1].match(/[+\-].*/)&&(o+=s)),(!n||e.getTime()>=n.getTime())&&(!a||e.getTime()<=a.getTime())&&(!r||e.getFullYear()>=r)&&(!o||o>=e.getFullYear())},_getFormatConfig:function(t){var e=this._get(t,"shortYearCutoff");return e="string"!=typeof e?e:(new Date).getFullYear()%100+parseInt(e,10),{shortYearCutoff:e,dayNamesShort:this._get(t,"dayNamesShort"),dayNames:this._get(t,"dayNames"),monthNamesShort:this._get(t,"monthNamesShort"),monthNames:this._get(t,"monthNames")}},_formatDate:function(t,e,i,s){e||(t.currentDay=t.selectedDay,t.currentMonth=t.selectedMonth,t.currentYear=t.selectedYear);var n=e?"object"==typeof e?e:this._daylightSavingAdjust(new Date(s,i,e)):this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return this.formatDate(this._get(t,"dateFormat"),n,this._getFormatConfig(t))}}),t.fn.datepicker=function(e){if(!this.length)return this;t.datepicker.initialized||(t(document).mousedown(t.datepicker._checkExternalClick),t.datepicker.initialized=!0),0===t("#"+t.datepicker._mainDivId).length&&t("body").append(t.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof e||"isDisabled"!==e&&"getDate"!==e&&"widget"!==e?"option"===e&&2===arguments.length&&"string"==typeof arguments[1]?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof e?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this].concat(i)):t.datepicker._attachDatepicker(this,e)}):t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i))},t.datepicker=new i,t.datepicker.initialized=!1,t.datepicker.uuid=(new Date).getTime(),t.datepicker.version="1.10.2",window["DP_jQuery_"+o]=t})(jQuery); \ No newline at end of file diff --git a/dist/jquery-editable/js/jquery-editable-poshytip.js b/dist/jquery-editable/js/jquery-editable-poshytip.js new file mode 100644 index 0000000..f41d3e5 --- /dev/null +++ b/dist/jquery-editable/js/jquery-editable-poshytip.js @@ -0,0 +1,4644 @@ +/*! X-editable - v1.4.4 +* 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); + }, + 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(); + + /** + Fired when rendering starts + @event rendering + @param {Object} event event object + **/ + this.$div.triggerHandler('rendering'); + + //init input + this.initInput(); + + //append input to form + this.input.prerender(); + 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'); + this.input.value2input(this.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 <br> for more pretty error display + if(msg) { + lines = msg.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('<div>').text(lines[i]).html(); + } + msg = lines.join('<br>'); + } + $group.addClass($.fn.editableform.errorGroupClass); + $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); + } + }, + + submit: function(e) { + e.stopPropagation(); + e.preventDefault(); + + var error, + newValue = this.input.input2value(); //get new value from input + + //validation + if (error = this.validate(newValue)) { + 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; + } + + //sending data to server + $.when(this.save(newValue)) + .done($.proxy(function(response) { + //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: <something>} --> 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 submitted value + @param {Object} params.response ajax response + + @example + $('#form-div').on('save'), function(e, params){ + if(params.newValue === 'username') {...} + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, response: response}); + }, this)) + .fail($.proxy(function(xhr) { + 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(newValue) { + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + //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 = !!(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 <code>text|textarea|select|date|checklist</code> + + @property type + @type string + @default 'text' + **/ + type: 'text', + /** + Url for submit, e.g. <code>'/post'</code> + 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 <code>object</code> - it is **appended** to original ajax data (pk, name and value). + If defined as <code>function</code> - 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 <code>id</code> 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. <code>{id: 1, lang: 'en'}</code>. + 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, + /** + Strategy for sending data on server. Can be <code>auto|always|never</code>. + When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element. + + @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. + + @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 <code>{success: true}</code> + or <code>{success: false, msg: "server error"}</code> 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** <code>{newValue: <something>}</code> - 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 <code>null</code> 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 = '<form class="form-inline editableform">'+ + '<div class="control-group">' + + '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+ + '<div class="editable-error-block"></div>' + + '</div>' + + '</form>'; + + //loading div + $.fn.editableform.loading = '<div class="editableform-loading"></div>'; + + //buttons + $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+ + '<button type="button" class="editable-cancel">cancel</button>'; + + //error class attached to control-group + $.fn.editableform.errorGroupClass = null; + + //error class attached to editable-error-block + $.fn.editableform.errorBlockClass = 'editable-error'; +}(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: <a data-source="{'a': 'b', 'c': 'd'}"> + * 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 $('<div>').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 []; + } + + valueProp = valueProp || 'value'; + + 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' ? o[valueProp] : o); }).length) { + result.push(o); + } + } else { + if(value == (o && typeof o === 'object' ? o[valueProp] : o)) { + 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; + } + } + + }; +}(window.jQuery)); + +/** +Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br> +This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br> +Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br> +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, //tbd in child class + innerCss: null, //tbd in child class + containerClass: 'editable-container editable-popup', //css class applied to container element + 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(); + + //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<exclude_classes.length; i++) { + if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) { + return; + } + } + + //close all open containers (except one - target) + Popup.prototype.closeOthers(e.target); + }); + + $(document).data('editable-handlers-attached', true); + } + }, + + //split options on containerOptions and formOptions + splitOptions: function() { + this.containerOptions = {}; + this.formOptions = {}; + + if(!$.fn[this.containerName]) { + throw new Error(this.containerName + ' not found. Have you included corresponding js file?'); + } + + var cDef = $.fn[this.containerName].defaults; + //keys defined in container defaults go to container, others go to form + for(var k in this.options) { + if(k in cDef) { + this.containerOptions[k] = this.options[k]; + } else { + this.formOptions[k] = this.options[k]; + } + } + }, + + /* + Returns jquery object of container + @method tip() + */ + tip: function() { + return this.container() ? this.container().$tip : null; + }, + + /* returns container object */ + container: function() { + return this.$element.data(this.containerDataName || this.containerName); + }, + + /* call native method of underlying container, e.g. this.$element.popover('method') */ + call: function() { + this.$element[this.containerName].apply(this.$element, arguments); + }, + + initContainer: function(){ + this.call(this.containerOptions); + }, + + renderForm: function() { + this.$form + .editableform(this.formOptions) + .on({ + save: $.proxy(this.save, this), //click on submit button (value changed) + nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed) + cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button + show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state) + rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown + resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed + rendered: $.proxy(function(){ + /** + Fired when container is shown and form is rendered (for select will wait for loading dropdown options). + **Note:** Bootstrap popover has own `shown` 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 shown + @param {Object} event event object + @example + $('#username').on('shown', function(e, editable) { + editable.input.$input.val('overwriting value of input..'); + }); + **/ + /* + TODO: added second param mainly to distinguish from bootstrap's shown event. It's a hotfix that will be solved in future versions via namespaced events. + */ + this.$element.triggerHandler('shown', this); + }, this) + }) + .editableform('render'); + }, + + /** + Shows container with form + @method show() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + /* Note: poshytip owerwrites this method totally! */ + show: function (closeAll) { + this.$element.addClass('editable-open'); + if(closeAll !== false) { + //close all open containers (except this) + this.closeOthers(this.$element[0]); + } + + //show container itself + this.innerShow(); + this.tip().addClass(this.containerClass); + + /* + Currently, form is re-rendered on every show. + The main reason is that we dont know, what container will do with content when closed: + remove(), detach() or just hide(). + + Detaching form itself before hide and re-insert before show is good solution, + but visually it looks ugly, as container changes size before hide. + */ + + //if form already exist - delete previous data + if(this.$form) { + //todo: destroy prev data! + //this.$form.destroy(); + } + + this.$form = $('<div>'); + + //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 <code>save|cancel|onblur|nochange|undefined (=manual)</code> + **/ + hide: function(reason) { + if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { + return; + } + + 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 <code>save|cancel|onblur|nochange|manual</code> + @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 <code>$(this).data('editableContainer')</code> 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 od 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 <code>top|right|bottom|left</code>. 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 <code>cancel|submit|ignore</code>. + Setting <code>ignore</code> allows to have several containers open. + + @property onblur + @type string + @default 'cancel' + @since 1.1.1 + **/ + onblur: 'cancel', + + /** + Animation speed (inline mode) + @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 <span> element + this.$tip = $('<span></span>'); + + //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(); + } + }; + + 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'); + + //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 + 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 + } + + //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; + } + + this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === ''; + + //emptytext shown only for enabled + if(!this.options.disabled) { + if (this.isEmpty) { + this.$element.text(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); + } + } + + //set new value + this.setValue(params.newValue, false, params.response); + + /** + Fired when new value was submitted. You can use <code>$(this).data('editable')</code> 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(); + } + + 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. + + @method getValue() + @returns {Object} object of element names and values + @example + $('#username, #fullname').editable('getValue'); + // possible result: + { + username: "superuser", + fullname: "John" + } + **/ + case 'getValue': + 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 <a href="#newrecord">creating new records</a> for details. + + @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'), + values; + + if($.isEmptyObject(errors)) { + values = this.editable('getValue'); + if(config.data) { + $.extend(values, config.data); + } + + $.ajax($.extend({ + url: config.url, + data: values, + type: 'POST' + }, config.ajaxOptions)) + .success(function(response) { + //successful response 200 OK + if(typeof config.success === 'function') { + config.success.call($elems, response, config); + } + }) + .error(function(){ //ajax error + if(typeof config.error === 'function') { + config.error.apply($elems, arguments); + } + }); + } 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; + + 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 <code>text|textarea|select|date|checklist</code> 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 <code>click|dblclick|mouseenter|manual</code>. + When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable. + **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element, + you need to apply <code>e.stopPropagation()</code> 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 <code>auto|always|never</code>. Useful for select and date. + For example, if dropdown list is <code>{1: 'a', 2: 'b'}</code> and element's value set to <code>1</code>, it's html will be automatically set to <code>'a'</code>. + <code>auto</code> - text will be automatically set only if element is empty. + <code>always|never</code> - 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 + <a id="price" data-type="text" data-value="100"></a> + <script> + $('#price').editable({ + ... + display: function(value) { + $(this).text(value + '$'); + } + }) + </script> + + @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 + <div id="user"> + <!-- empty --> + <a href="#" data-name="username" data-type="text" class="editable-click editable-empty" data-value="" title="Username">Empty</a> + <!-- non-empty --> + <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" class="editable-click" title="Group">Operator</a> + </div> + + <script> + $('#user').editable({ + selector: 'a', + url: '/post', + pk: 1 + }); + </script> + **/ + selector: null + }; + +}(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).text(value); + }, + + /** + Converts element's html to value + + @method html2value(html) + @param {string} html + @returns {mixed} + **/ + html2value: function(html) { + return $('<div>').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 $('<div>').text(str).html(); + }, + + /** + attach handler to automatically submit form when value changed (useful when buttons not shown) + **/ + autosubmit: 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 input-medium + **/ + inputclass: 'input-medium', + //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) { + //if allready loaded just call success + if($.isArray(this.sourceData)) { + success.call(this); + return; + } + + // try parse json in single quotes (for double quotes jquery does automatically) + try { + this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false); + } catch (e) { + error.call(this); + return; + } + + var source = this.options.source; + + //run source if it function + if ($.isFunction(source)) { + source = source.call(this.options.scope); + } + + //loading from url + if (typeof source === 'string') { + //try to get 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 = []; + } + } + + //loading sourceData from server + $.ajax({ + 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) + }); + } 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 <code>true</code> 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 + }); + + $.fn.editabletypes.list = List; + +}(window.jQuery)); + +/** +Text input + +@class text +@extends abstractinput +@final +@example +<a href="#" id="username" data-type="text" data-pk="1">awesome</a> +<script> +$(function(){ + $('#username').editable({ + url: '/post', + title: 'Enter username' + }); +}); +</script> +**/ +(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 = $('<span class="editable-clear-x"></span>'); + 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 <input type="text"> + **/ + tpl: '<input type="text">', + /** + 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 +<a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a> +<script> +$(function(){ + $('#comments').editable({ + url: '/post', + title: 'Enter comments', + rows: 10 + }); +}); +</script> +**/ +(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(); + } + }); + }, + + value2html: function(value, element) { + var html = '', lines; + if(value) { + lines = value.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('<div>').text(lines[i]).html(); + } + html = lines.join('<br>'); + } + $(element).html(html); + }, + + html2value: function(html) { + if(!html) { + return ''; + } + + var regex = new RegExp(String.fromCharCode(10), 'g'); + var lines = html.split(/<br\s*\/?>/i); + for (var i = 0; i < lines.length; i++) { + var text = $('<div>').html(lines[i]).text(); + + // Remove newline characters (\n) to avoid them being converted by value2html() method + // thus adding extra <br> 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 <textarea></textarea> + **/ + tpl:'<textarea></textarea>', + /** + @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 +<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a> +<script> +$(function(){ + $('#status').editable({ + value: 2, + source: [ + {value: 1, text: 'Active'}, + {value: 2, text: 'Blocked'}, + {value: 3, text: 'Deleted'} + ] + }); +}); +</script> +**/ +(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) { + if($.isArray(data)) { + for(var i=0; i<data.length; i++) { + if(data[i].children) { + $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children)); + } else { + $el.append($('<option>', {value: data[i].value}).text(data[i].text)); + } + } + } + return $el; + }; + + fillItems(this.$input, this.sourceData); + + this.setClass(); + + //enter submit + this.$input.on('keydown.editable', function (e) { + if (e.which === 13) { + $(this).closest('form').submit(); + } + }); + }, + + value2htmlFinal: function(value, element) { + var text = '', + items = $.fn.editableutils.itemsByValue(value, this.sourceData); + + if(items.length) { + text = items[0].text; + } + + $(element).text(text); + }, + + autosubmit: function() { + this.$input.off('keydown.editable').on('change.editable', function(){ + $(this).closest('form').submit(); + }); + } + }); + + Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { + /** + @property tpl + @default <select></select> + **/ + tpl:'<select></select>' + }); + + $.fn.editabletypes.select = Select; + +}(window.jQuery)); + +/** +List of checkboxes. +Internally value stored as javascript array of values. + +@class checklist +@extends list +@final +@example +<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-original-title="Select options"></a> +<script> +$(function(){ + $('#options').editable({ + value: [2, 3], + source: [ + {value: 1, text: 'option1'}, + {value: 2, text: 'option2'}, + {value: 3, text: 'option3'} + ] + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Checklist = function (options) { + this.init('checklist', options, Checklist.defaults); + }; + + $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); + + $.extend(Checklist.prototype, { + renderList: function() { + var $label, $div; + + this.$tpl.empty(); + + if(!$.isArray(this.sourceData)) { + return; + } + + for(var i=0; i<this.sourceData.length; i++) { + $label = $('<label>').append($('<input>', { + type: 'checkbox', + value: this.sourceData[i].value + })) + .append($('<span>').text(' '+this.sourceData[i].text)); + + $('<div>').append($label).appendTo(this.$tpl); + } + + this.$input = this.$tpl.find('input[type="checkbox"]'); + this.setClass(); + }, + + value2str: function(value) { + return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : ''; + }, + + //parse separated string + str2value: function(str) { + var reg, value = null; + if(typeof str === 'string' && str.length) { + reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*'); + value = str.split(reg); + } else if($.isArray(str)) { + value = str; + } else { + value = [str]; + } + return value; + }, + + //set checked on required checkboxes + value2input: function(value) { + this.$input.prop('checked', false); + if($.isArray(value) && value.length) { + this.$input.each(function(i, el) { + var $el = $(el); + // cannot use $.inArray as it performs strict comparison + $.each(value, function(j, val){ + /*jslint eqeq: true*/ + if($el.val() == val) { + /*jslint eqeq: false*/ + $el.prop('checked', true); + } + }); + }); + } + }, + + input2value: function() { + var checked = []; + this.$input.filter(':checked').each(function(i, el) { + checked.push($(el).val()); + }); + return checked; + }, + + //collect text of checked boxes + value2htmlFinal: function(value, element) { + var html = [], + checked = $.fn.editableutils.itemsByValue(value, this.sourceData); + + if(checked.length) { + $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); + $(element).html(html.join('<br>')); + } else { + $(element).empty(); + } + }, + + activate: function() { + this.$input.first().focus(); + }, + + autosubmit: function() { + this.$input.on('keydown', function(e){ + if (e.which === 13) { + $(this).closest('form').submit(); + } + }); + } + }); + + Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { + /** + @property tpl + @default <div></div> + **/ + tpl:'<div class="editable-checklist"></div>', + + /** + @property inputclass + @type string + @default null + **/ + inputclass: null, + + /** + Separator of values when reading from `data-value` attribute + + @property separator + @type string + @default ',' + **/ + separator: ',' + }); + + $.fn.editabletypes.checklist = Checklist; + +}(window.jQuery)); + +/** +HTML5 input types. +Following types are supported: + +* password +* email +* url +* tel +* number +* range + +Learn more about html5 inputs: +http://www.w3.org/wiki/HTML5_form_additions +To check browser compatibility please see: +https://developer.mozilla.org/en-US/docs/HTML/Element/Input + +@class html5types +@extends text +@final +@since 1.3.0 +@example +<a href="#" id="email" data-type="email" data-pk="1">admin@example.com</a> +<script> +$(function(){ + $('#email').editable({ + url: '/post', + title: 'Enter email' + }); +}); +</script> +**/ + +/** +@property tpl +@default depends on type +**/ + +/* +Password +*/ +(function ($) { + "use strict"; + + var Password = function (options) { + this.init('password', options, Password.defaults); + }; + $.fn.editableutils.inherit(Password, $.fn.editabletypes.text); + $.extend(Password.prototype, { + //do not display password, show '[hidden]' instead + value2html: function(value, element) { + if(value) { + $(element).text('[hidden]'); + } else { + $(element).empty(); + } + }, + //as password not displayed, should not set value by html + html2value: function(html) { + return null; + } + }); + Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="password">' + }); + $.fn.editabletypes.password = Password; +}(window.jQuery)); + + +/* +Email +*/ +(function ($) { + "use strict"; + + var Email = function (options) { + this.init('email', options, Email.defaults); + }; + $.fn.editableutils.inherit(Email, $.fn.editabletypes.text); + Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="email">' + }); + $.fn.editabletypes.email = Email; +}(window.jQuery)); + + +/* +Url +*/ +(function ($) { + "use strict"; + + var Url = function (options) { + this.init('url', options, Url.defaults); + }; + $.fn.editableutils.inherit(Url, $.fn.editabletypes.text); + Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="url">' + }); + $.fn.editabletypes.url = Url; +}(window.jQuery)); + + +/* +Tel +*/ +(function ($) { + "use strict"; + + var Tel = function (options) { + this.init('tel', options, Tel.defaults); + }; + $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text); + Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="tel">' + }); + $.fn.editabletypes.tel = Tel; +}(window.jQuery)); + + +/* +Number +*/ +(function ($) { + "use strict"; + + var NumberInput = function (options) { + this.init('number', options, NumberInput.defaults); + }; + $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text); + $.extend(NumberInput.prototype, { + render: function () { + NumberInput.superclass.render.call(this); + this.setAttr('min'); + this.setAttr('max'); + this.setAttr('step'); + }, + postrender: function() { + if(this.$clear) { + //increase right ffset for up/down arrows + this.$clear.css({right: 24}); + /* + //can position clear button only here, when form is shown and height can be calculated + var h = this.$input.outerHeight(true) || 20, + delta = (h - this.$clear.height()) / 2; + + //add 12px to offset right for up/down arrows + this.$clear.css({top: delta, right: delta + 16}); + */ + } + } + }); + NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="number">', + inputclass: 'input-mini', + min: null, + max: null, + step: null + }); + $.fn.editabletypes.number = NumberInput; +}(window.jQuery)); + + +/* +Range (inherit from number) +*/ +(function ($) { + "use strict"; + + var Range = function (options) { + this.init('range', options, Range.defaults); + }; + $.fn.editableutils.inherit(Range, $.fn.editabletypes.number); + $.extend(Range.prototype, { + render: function () { + this.$input = this.$tpl.filter('input'); + + this.setClass(); + this.setAttr('min'); + this.setAttr('max'); + this.setAttr('step'); + + this.$input.on('input', function(){ + $(this).siblings('output').text($(this).val()); + }); + }, + activate: function() { + this.$input.focus(); + } + }); + Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, { + tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>', + inputclass: 'input-medium' + }); + $.fn.editabletypes.range = Range; +}(window.jQuery)); +/** +Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2. +Please see [original docs](http://ivaynberg.github.com/select2) for detailed description and options. +You should manually include select2 distributive: + + <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> + <script src="select2/select2.js"></script> + +For make it **Bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css): + + <link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link> + +**Note:** currently `ajax` source for select2 is not supported, as it's not possible to load it in closed select2 state. +The solution is to load source manually and assign statically. + +@class select2 +@extends abstractinput +@since 1.4.1 +@final +@example +<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-original-title="Select country"></a> +<script> +$(function(){ + $('#country').editable({ + source: [ + {id: 'gb', text: 'Great Britain'}, + {id: 'us', text: 'United States'}, + {id: 'ru', text: 'Russia'} + ], + select2: { + multiple: true + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Constructor = function (options) { + this.init('select2', options, Constructor.defaults); + + options.select2 = options.select2 || {}; + + var that = this, + mixin = { //mixin to select2 options + placeholder: options.placeholder + }; + + //detect whether it is multi-valued + this.isMultiple = options.select2.tags || options.select2.multiple; + + //if not `tags` mode, we need define initSelection to set data from source + if(!options.select2.tags) { + if(options.source) { + mixin.data = options.source; + } + + //this function can be defaulted in seletc2. See https://github.com/ivaynberg/select2/issues/710 + mixin.initSelection = function (element, callback) { + //temp: try update results + /* + if(options.select2 && options.select2.ajax) { + console.log('attached'); + var original = $(element).data('select2').postprocessResults; + console.log(original); + $(element).data('select2').postprocessResults = function(data, initial) { + console.log('postprocess'); + // this.element.triggerHandler('loaded', [data]); + original.apply(this, arguments); + } + + // $(element).on('loaded', function(){console.log('loaded');}); + $(element).data('select2').updateResults(true); + } + */ + + var val = that.str2value(element.val()), + data = $.fn.editableutils.itemsByValue(val, mixin.data, 'id'); + + //for single-valued mode should not use array. Take first element instead. + if($.isArray(data) && data.length && !that.isMultiple) { + data = data[0]; + } + + callback(data); + }; + } + + //overriding objects in config (as by default jQuery extend() is not recursive) + this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2); + }; + + $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); + + $.extend(Constructor.prototype, { + render: function() { + this.setClass(); + //apply select2 + this.$input.select2(this.options.select2); + + //when data is loaded via ajax, we need to know when it's done + if('ajax' in this.options.select2) { + /* + console.log('attached'); + var original = this.$input.data('select2').postprocessResults; + this.$input.data('select2').postprocessResults = function(data, initial) { + this.element.triggerHandler('loaded', [data]); + original.apply(this, arguments); + } + */ + } + + + //trigger resize of editableform to re-position container in multi-valued mode + if(this.isMultiple) { + this.$input.on('change', function() { + $(this).closest('form').parent().triggerHandler('resize'); + }); + } + }, + + value2html: function(value, element) { + var text = '', data; + if(this.$input) { //called when submitting form and select2 already exists + data = this.$input.select2('data'); + } else { //on init (autotext) + //here select2 instance not created yet and data may be even not loaded. + //we can check data/tags property of select config and if exist lookup text + if(this.options.select2.tags) { + data = value; + } else if(this.options.select2.data) { + data = $.fn.editableutils.itemsByValue(value, this.options.select2.data, 'id'); + } else { + //if('ajax' in this.options.select2) { + } + } + + if($.isArray(data)) { + //collect selected data and show with separator + text = []; + $.each(data, function(k, v){ + text.push(v && typeof v === 'object' ? v.text : v); + }); + } else if(data) { + text = data.text; + } + + text = $.isArray(text) ? text.join(this.options.viewseparator) : text; + + $(element).text(text); + }, + + html2value: function(html) { + return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null; + }, + + value2input: function(value) { + this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + }, + + input2value: function() { + return this.$input.select2('val'); + }, + + str2value: function(str, separator) { + if(typeof str !== 'string' || !this.isMultiple) { + return str; + } + + separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator; + + var val, i, l; + + if (str === null || str.length < 1) { + return null; + } + val = str.split(separator); + for (i = 0, l = val.length; i < l; i = i + 1) { + val[i] = $.trim(val[i]); + } + + return val; + }, + + autosubmit: function() { + this.$input.on('change', function(e, isInitial){ + if(!isInitial) { + $(this).closest('form').submit(); + } + }); + } + + }); + + Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <input type="hidden"> + **/ + tpl:'<input type="hidden">', + /** + Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2). + + @property select2 + @type object + @default null + **/ + select2: null, + /** + Placeholder attribute of select + + @property placeholder + @type string + @default null + **/ + placeholder: null, + /** + Source data for select. It will be assigned to select2 `data` property and kept here just for convenience. + Please note, that format is different from simple `select` input: use 'id' instead of 'value'. + E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`. + + @property source + @type array + @default null + **/ + source: null, + /** + Separator used to display tags. + + @property viewseparator + @type string + @default ', ' + **/ + viewseparator: ', ' + }); + + $.fn.editabletypes.select2 = Constructor; + +}(window.jQuery)); + +/** +* Combodate - 1.0.3 +* Dropdown date and time picker. +* Converts text input into dropdowns to pick day, month, year, hour, minute and second. +* Uses momentjs as datetime library http://momentjs.com. +* For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang +* +* Author: Vitaliy Potapov +* Project page: http://github.com/vitalets/combodate +* Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. +**/ +(function ($) { + + var Combodate = function (element, options) { + this.$element = $(element); + if(!this.$element.is('input')) { + $.error('Combodate should be applied to INPUT element'); + return; + } + this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data()); + this.init(); + }; + + Combodate.prototype = { + constructor: Combodate, + init: function () { + this.map = { + //key regexp moment.method + day: ['D', 'date'], + month: ['M', 'month'], + year: ['Y', 'year'], + hour: ['[Hh]', 'hours'], + minute: ['m', 'minutes'], + second: ['s', 'seconds'], + ampm: ['[Aa]', ''] + }; + + this.$widget = $('<span class="combodate"></span>').html(this.getTemplate()); + + this.initCombos(); + + //update original input on change + this.$widget.on('change', 'select', $.proxy(function(){ + this.$element.val(this.getValue()); + }, this)); + + this.$widget.find('select').css('width', 'auto'); + + //hide original input and insert widget + this.$element.hide().after(this.$widget); + + //set initial value + this.setValue(this.$element.val() || this.options.value); + }, + + /* + Replace tokens in template with <select> elements + */ + getTemplate: function() { + var tpl = this.options.template; + + //first pass + $.each(this.map, function(k, v) { + v = v[0]; + var r = new RegExp(v+'+'), + token = v.length > 1 ? v.substring(1, 2) : v; + + tpl = tpl.replace(r, '{'+token+'}'); + }); + + //replace spaces with + tpl = tpl.replace(/ /g, ' '); + + //second pass + $.each(this.map, function(k, v) { + v = v[0]; + var token = v.length > 1 ? v.substring(1, 2) : v; + + tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>'); + }); + + return tpl; + }, + + /* + Initialize combos that presents in template + */ + initCombos: function() { + var that = this; + $.each(this.map, function(k, v) { + var $c = that.$widget.find('.'+k), f, items; + if($c.length) { + that['$'+k] = $c; //set properties like this.$day, this.$month etc. + f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); //define method name to fill items, e.g `fillDays` + items = that[f](); + that['$'+k].html(that.renderItems(items)); + } + }); + }, + + /* + Initialize items of combos. Handles `firstItem` option + */ + initItems: function(key) { + var values = [], + relTime; + + if(this.options.firstItem === 'name') { + //need both to support moment ver < 2 and >= 2 + relTime = moment.relativeTime || moment.langData()._relativeTime; + var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key]; + //take last entry (see momentjs lang files structure) + header = header.split(' ').reverse()[0]; + values.push(['', header]); + } else if(this.options.firstItem === 'empty') { + values.push(['', '']); + } + return values; + }, + + /* + render items to string of <option> tags + */ + renderItems: function(items) { + var str = []; + for(var i=0; i<items.length; i++) { + str.push('<option value="'+items[i][0]+'">'+items[i][1]+'</option>'); + } + return str.join("\n"); + }, + + /* + fill day + */ + fillDay: function() { + var items = this.initItems('d'), name, i, + twoDigit = this.options.template.indexOf('DD') !== -1; + + for(i=1; i<=31; i++) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill month + */ + fillMonth: function() { + var items = this.initItems('M'), name, i, + longNames = this.options.template.indexOf('MMMM') !== -1, + shortNames = this.options.template.indexOf('MMM') !== -1, + twoDigit = this.options.template.indexOf('MM') !== -1; + + for(i=0; i<=11; i++) { + if(longNames) { + name = moment().month(i).format('MMMM'); + } else if(shortNames) { + name = moment().month(i).format('MMM'); + } else if(twoDigit) { + name = this.leadZero(i+1); + } else { + name = i+1; + } + items.push([i, name]); + } + return items; + }, + + /* + fill year + */ + fillYear: function() { + var items = [], name, i, + longNames = this.options.template.indexOf('YYYY') !== -1; + + for(i=this.options.maxYear; i>=this.options.minYear; i--) { + name = longNames ? i : (i+'').substring(2); + items[this.options.yearDescending ? 'push' : 'unshift']([i, name]); + } + + items = this.initItems('y').concat(items); + + return items; + }, + + /* + fill hour + */ + fillHour: function() { + var items = this.initItems('h'), name, i, + h12 = this.options.template.indexOf('h') !== -1, + h24 = this.options.template.indexOf('H') !== -1, + twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1, + max = h12 ? 12 : 23; + + for(i=0; i<=max; i++) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill minute + */ + fillMinute: function() { + var items = this.initItems('m'), name, i, + twoDigit = this.options.template.indexOf('mm') !== -1; + + for(i=0; i<=59; i+= this.options.minuteStep) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill second + */ + fillSecond: function() { + var items = this.initItems('s'), name, i, + twoDigit = this.options.template.indexOf('ss') !== -1; + + for(i=0; i<=59; i+= this.options.secondStep) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill ampm + */ + fillAmpm: function() { + var ampmL = this.options.template.indexOf('a') !== -1, + ampmU = this.options.template.indexOf('A') !== -1, + items = [ + ['am', ampmL ? 'am' : 'AM'], + ['pm', ampmL ? 'pm' : 'PM'] + ]; + return items; + }, + + /* + Returns current date value. + If format not specified - `options.format` used. + If format = `null` - Moment object returned. + */ + getValue: function(format) { + var dt, values = {}, + that = this, + notSelected = false; + + //getting selected values + $.each(this.map, function(k, v) { + if(k === 'ampm') { + return; + } + var def = k === 'day' ? 1 : 0; + + values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def; + + if(isNaN(values[k])) { + notSelected = true; + return false; + } + }); + + //if at least one visible combo not selected - return empty string + if(notSelected) { + return ''; + } + + //convert hours if 12h format + if(this.$ampm) { + values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; + if(values.hour === 24) { + values.hour = 0; + } + } + + dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]); + + //highlight invalid date + this.highlight(dt); + + format = format === undefined ? this.options.format : format; + if(format === null) { + return dt.isValid() ? dt : null; + } else { + return dt.isValid() ? dt.format(format) : ''; + } + }, + + setValue: function(value) { + if(!value) { + return; + } + + var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value), + that = this, + values = {}; + + //function to find nearest value in select options + function getNearest($select, value) { + var delta = {}; + $select.children('option').each(function(i, opt){ + var optValue = $(opt).attr('value'), + distance; + + if(optValue === '') return; + distance = Math.abs(optValue - value); + if(typeof delta.distance === 'undefined' || distance < delta.distance) { + delta = {value: optValue, distance: distance}; + } + }); + return delta.value; + } + + if(dt.isValid()) { + //read values from date object + $.each(this.map, function(k, v) { + if(k === 'ampm') { + return; + } + values[k] = dt[v[1]](); + }); + + if(this.$ampm) { + if(values.hour > 12) { + values.hour -= 12; + values.ampm = 'pm'; + } else { + values.ampm = 'am'; + } + } + + $.each(values, function(k, v) { + //call val() for each existing combo, e.g. this.$hour.val() + if(that['$'+k]) { + + if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } + + if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } + + that['$'+k].val(v); + } + }); + + this.$element.val(dt.format(this.options.format)); + } + }, + + /* + highlight combos if date is invalid + */ + highlight: function(dt) { + if(!dt.isValid()) { + if(this.options.errorClass) { + this.$widget.addClass(this.options.errorClass); + } else { + //store original border color + if(!this.borderColor) { + this.borderColor = this.$widget.find('select').css('border-color'); + } + this.$widget.find('select').css('border-color', 'red'); + } + } else { + if(this.options.errorClass) { + this.$widget.removeClass(this.options.errorClass); + } else { + this.$widget.find('select').css('border-color', this.borderColor); + } + } + }, + + leadZero: function(v) { + return v <= 9 ? '0' + v : v; + }, + + destroy: function() { + this.$widget.remove(); + this.$element.removeData('combodate').show(); + } + + //todo: clear method + }; + + $.fn.combodate = function ( option ) { + var d, args = Array.apply(null, arguments); + args.shift(); + + //getValue returns date as string / object (not jQuery object) + if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) { + return d.getValue.apply(d, args); + } + + return this.each(function () { + var $this = $(this), + data = $this.data('combodate'), + options = typeof option == 'object' && option; + if (!data) { + $this.data('combodate', (data = new Combodate(this, options))); + } + if (typeof option == 'string' && typeof data[option] == 'function') { + data[option].apply(data, args); + } + }); + }; + + $.fn.combodate.defaults = { + //in this format value stored in original input + format: 'DD-MM-YYYY HH:mm', + //in this format items in dropdowns are displayed + template: 'D / MMM / YYYY H : mm', + //initial value, can be `new Date()` + value: null, + minYear: 1970, + maxYear: 2015, + yearDescending: true, + minuteStep: 5, + secondStep: 1, + firstItem: 'empty', //'name', 'empty', 'none' + errorClass: null, + roundTime: true //whether to round minutes and seconds if step > 1 + }; + +}(window.jQuery)); +/** +Combodate input - dropdown date and time picker. +Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com). + + <script src="js/moment.min.js"></script> + +Allows to input: + +* only date +* only time +* both date and time + +Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker. +Internally value stored as `momentjs` object. + +@class combodate +@extends abstractinput +@final +@since 1.4.0 +@example +<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-original-title="Select date"></a> +<script> +$(function(){ + $('#dob').editable({ + format: 'YYYY-MM-DD', + viewformat: 'DD.MM.YYYY', + template: 'D / MMMM / YYYY', + combodate: { + minYear: 2000, + maxYear: 2015, + minuteStep: 1 + } + } + }); +}); +</script> +**/ + +/*global moment*/ + +(function ($) { + "use strict"; + + var Constructor = function (options) { + this.init('combodate', options, Constructor.defaults); + + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } + + //try parse combodate config defined as json string in data-combodate + options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true); + + //overriding combodate config (as by default jQuery extend() is not recursive) + this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, { + format: this.options.format, + template: this.options.template + }); + }; + + $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); + + $.extend(Constructor.prototype, { + render: function () { + this.$input.combodate(this.options.combodate); + + //"clear" link + /* + if(this.options.clear) { + this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ + e.preventDefault(); + e.stopPropagation(); + this.clear(); + }, this)); + + this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); + } + */ + }, + + value2html: function(value, element) { + var text = value ? value.format(this.options.viewformat) : ''; + $(element).text(text); + }, + + html2value: function(html) { + return html ? moment(html, this.options.viewformat) : null; + }, + + value2str: function(value) { + return value ? value.format(this.options.format) : ''; + }, + + str2value: function(str) { + return str ? moment(str, this.options.format) : null; + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + this.$input.combodate('setValue', value); + }, + + input2value: function() { + return this.$input.combodate('getValue', null); + }, + + activate: function() { + this.$input.siblings('.combodate').find('select').eq(0).focus(); + }, + + /* + clear: function() { + this.$input.data('datepicker').date = null; + this.$input.find('.active').removeClass('active'); + }, + */ + + autosubmit: function() { + + } + + }); + + Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <input type="text"> + **/ + tpl:'<input type="text">', + /** + @property inputclass + @default null + **/ + inputclass: null, + /** + Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br> + See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format) + + @property format + @type string + @default YYYY-MM-DD + **/ + format:'YYYY-MM-DD', + /** + Format used for displaying date. Also applied when converting date from element's text on init. + If not specified equals to `format`. + + @property viewformat + @type string + @default null + **/ + viewformat: null, + /** + Template used for displaying dropdowns. + + @property template + @type string + @default D / MMM / YYYY + **/ + template: 'D / MMM / YYYY', + /** + Configuration of combodate. + Full list of options: http://vitalets.github.com/combodate/#docs + + @property combodate + @type object + @default null + **/ + combodate: null + + /* + (not implemented yet) + Text shown as clear date button. + If <code>false</code> clear button will not be rendered. + + @property clear + @type boolean|string + @default 'x clear' + */ + //clear: '× clear' + }); + + $.fn.editabletypes.combodate = Constructor; + +}(window.jQuery)); + +/** +* Editable Poshytip +* --------------------- +* requires jquery.poshytip.js +*/ +(function ($) { + "use strict"; + + //extend methods + $.extend($.fn.editableContainer.Popup.prototype, { + containerName: 'poshytip', + innerCss: 'div.tip-inner', + + initContainer: function(){ + this.handlePlacement(); + + $.extend(this.containerOptions, { + showOn: 'none', + content: '', + alignTo: 'target' + }); + + this.call(this.containerOptions); + }, + + /* + Overwrite totally show() method as poshytip requires content is set before show + */ + show: function (closeAll) { + this.$element.addClass('editable-open'); + if(closeAll !== false) { + //close all open containers (except this) + this.closeOthers(this.$element[0]); + } + + //render form + this.$form = $('<div>'); + this.renderForm(); + + var $label = $('<label>').text(this.options.title || this.$element.data( "title") || this.$element.data( "originalTitle")), + $content = $('<div>').append($label).append(this.$form); + + this.call('update', $content); + this.call('show'); + + this.tip().addClass('editable-container'); + this.$form.data('editableform').input.activate(); + }, + + /* hide */ + innerHide: function () { + this.call('hide'); + }, + + /* destroy */ + innerDestroy: function() { + this.call('destroy'); + }, + + setPosition: function() { + this.container().refresh(false); + }, + + handlePlacement: function() { + var x, y, ox = 0, oy = 0; + switch(this.options.placement) { + case 'top': + x = 'center'; + y = 'top'; + oy = 5; + break; + case 'right': + x = 'right'; + y = 'center'; + ox = 10; + break; + case 'bottom': + x = 'center'; + y = 'bottom'; + oy = 5; + break; + case 'left': + x = 'left'; + y = 'center'; + ox = 10; + break; + } + + $.extend(this.containerOptions, { + alignX: x, + offsetX: ox, + alignY: y, + offsetY:oy + }); + } + }); + + //defaults + $.fn.editableContainer.defaults = $.extend({}, $.fn.editableContainer.defaults, { + className: 'tip-yellowsimple' + }); + + + /** + * Poshytip fix: disable incorrect table display + * see https://github.com/vadikom/poshytip/issues/7 + */ + /*jshint eqeqeq:false, curly: false*/ + if($.Poshytip) { //need this check, because in inline mode poshytip may not be loaded! + var tips = [], + reBgImage = /^url\(["']?([^"'\)]*)["']?\);?$/i, + rePNG = /\.png$/i, + ie6 = !!window.createPopup && document.documentElement.currentStyle.minWidth == 'undefined'; + + $.Poshytip.prototype.refresh = function(async) { + if (this.disabled) + return; + + var currPos; + if (async) { + if (!this.$tip.data('active')) + return; + // save current position as we will need to animate + currPos = {left: this.$tip.css('left'), top: this.$tip.css('top')}; + } + + // reset position to avoid text wrapping, etc. + this.$tip.css({left: 0, top: 0}).appendTo(document.body); + + // save default opacity + if (this.opacity === undefined) + this.opacity = this.$tip.css('opacity'); + + // check for images - this code is here (i.e. executed each time we show the tip and not on init) due to some browser inconsistencies + var bgImage = this.$tip.css('background-image').match(reBgImage), + arrow = this.$arrow.css('background-image').match(reBgImage); + + if (bgImage) { + var bgImagePNG = rePNG.test(bgImage[1]); + // fallback to background-color/padding/border in IE6 if a PNG is used + if (ie6 && bgImagePNG) { + this.$tip.css('background-image', 'none'); + this.$inner.css({margin: 0, border: 0, padding: 0}); + bgImage = bgImagePNG = false; + } else { + this.$tip.prepend('<table class="fallback" border="0" cellpadding="0" cellspacing="0"><tr><td class="tip-top tip-bg-image" colspan="2"><span></span></td><td class="tip-right tip-bg-image" rowspan="2"><span></span></td></tr><tr><td class="tip-left tip-bg-image" rowspan="2"><span></span></td><td></td></tr><tr><td class="tip-bottom tip-bg-image" colspan="2"><span></span></td></tr></table>') + .css({border: 0, padding: 0, 'background-image': 'none', 'background-color': 'transparent'}) + .find('.tip-bg-image').css('background-image', 'url("' + bgImage[1] +'")').end() + .find('td').eq(3).append(this.$inner); + } + // disable fade effect in IE due to Alpha filter + translucent PNG issue + if (bgImagePNG && !$.support.opacity) + this.opts.fade = false; + } + // IE arrow fixes + if (arrow && !$.support.opacity) { + // disable arrow in IE6 if using a PNG + if (ie6 && rePNG.test(arrow[1])) { + arrow = false; + this.$arrow.css('background-image', 'none'); + } + // disable fade effect in IE due to Alpha filter + translucent PNG issue + this.opts.fade = false; + } + + var $table = this.$tip.find('table.fallback'); + if (ie6) { + // fix min/max-width in IE6 + this.$tip[0].style.width = ''; + $table.width('auto').find('td').eq(3).width('auto'); + var tipW = this.$tip.width(), + minW = parseInt(this.$tip.css('min-width'), 10), + maxW = parseInt(this.$tip.css('max-width'), 10); + if (!isNaN(minW) && tipW < minW) + tipW = minW; + else if (!isNaN(maxW) && tipW > maxW) + tipW = maxW; + this.$tip.add($table).width(tipW).eq(0).find('td').eq(3).width('100%'); + } else if ($table[0]) { + // fix the table width if we are using a background image + // IE9, FF4 use float numbers for width/height so use getComputedStyle for them to avoid text wrapping + // for details look at: http://vadikom.com/dailies/offsetwidth-offsetheight-useless-in-ie9-firefox4/ + $table.width('auto').find('td').eq(3).width('auto').end().end().width(document.defaultView && document.defaultView.getComputedStyle && parseFloat(document.defaultView.getComputedStyle(this.$tip[0], null).width) || this.$tip.width()).find('td').eq(3).width('100%'); + } + this.tipOuterW = this.$tip.outerWidth(); + this.tipOuterH = this.$tip.outerHeight(); + + this.calcPos(); + + // position and show the arrow image + if (arrow && this.pos.arrow) { + this.$arrow[0].className = 'tip-arrow tip-arrow-' + this.pos.arrow; + this.$arrow.css('visibility', 'inherit'); + } + + if (async) { + this.asyncAnimating = true; + var self = this; + this.$tip.css(currPos).animate({left: this.pos.l, top: this.pos.t}, 200, function() { self.asyncAnimating = false; }); + } else { + this.$tip.css({left: this.pos.l, top: this.pos.t}); + } + }; + } + /*jshinteqeqeq: true, curly: true*/ +}(window.jQuery)); +/** +jQuery UI Datepicker. +Description and examples: http://jqueryui.com/datepicker. +This input is also accessible as **date** type. Do not use it together with __bootstrap-datepicker__ as both apply <code>$().datepicker()</code> method. +For **i18n** you should include js file from here: https://github.com/jquery/jquery-ui/tree/master/ui/i18n. + +@class dateui +@extends abstractinput +@final +@example +<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a> +<script> +$(function(){ + $('#dob').editable({ + format: 'yyyy-mm-dd', + viewformat: 'dd/mm/yyyy', + datepicker: { + firstDay: 1 + } + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var DateUI = function (options) { + this.init('dateui', options, DateUI.defaults); + this.initPicker(options, DateUI.defaults); + }; + + $.fn.editableutils.inherit(DateUI, $.fn.editabletypes.abstractinput); + + $.extend(DateUI.prototype, { + initPicker: function(options, defaults) { + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } + + //correct formats: replace yyyy with yy (for compatibility with bootstrap datepicker) + this.options.viewformat = this.options.viewformat.replace('yyyy', 'yy'); + this.options.format = this.options.format.replace('yyyy', 'yy'); + + //overriding datepicker config (as by default jQuery extend() is not recursive) + //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only + this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, { + dateFormat: this.options.viewformat + }); + }, + + render: function () { + this.$input.datepicker(this.options.datepicker); + + //"clear" link + if(this.options.clear) { + this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ + e.preventDefault(); + e.stopPropagation(); + this.clear(); + }, this)); + + this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); + } + }, + + value2html: function(value, element) { + var text = $.datepicker.formatDate(this.options.viewformat, value); + DateUI.superclass.value2html(text, element); + }, + + html2value: function(html) { + if(typeof html !== 'string') { + return html; + } + + //if string does not match format, UI datepicker throws exception + var d; + try { + d = $.datepicker.parseDate(this.options.viewformat, html); + } catch(e) {} + + return d; + }, + + value2str: function(value) { + return $.datepicker.formatDate(this.options.format, value); + }, + + str2value: function(str) { + if(typeof str !== 'string') { + return str; + } + + //if string does not match format, UI datepicker throws exception + var d; + try { + d = $.datepicker.parseDate(this.options.format, str); + } catch(e) {} + + return d; + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + this.$input.datepicker('setDate', value); + }, + + input2value: function() { + return this.$input.datepicker('getDate'); + }, + + activate: function() { + }, + + clear: function() { + this.$input.datepicker('setDate', null); + }, + + autosubmit: function() { + this.$input.on('mouseup', 'table.ui-datepicker-calendar a.ui-state-default', function(e){ + var $form = $(this).closest('form'); + setTimeout(function() { + $form.submit(); + }, 200); + }); + } + + }); + + DateUI.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <div></div> + **/ + tpl:'<div class="editable-date"></div>', + /** + @property inputclass + @default null + **/ + inputclass: null, + /** + Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br> + Full list of tokens: http://docs.jquery.com/UI/Datepicker/formatDate + + @property format + @type string + @default yyyy-mm-dd + **/ + format:'yyyy-mm-dd', + /** + Format used for displaying date. Also applied when converting date from element's text on init. + If not specified equals to <code>format</code> + + @property viewformat + @type string + @default null + **/ + viewformat: null, + + /** + Configuration of datepicker. + Full list of options: http://api.jqueryui.com/datepicker + + @property datepicker + @type object + @default { + firstDay: 0, + changeYear: true, + changeMonth: true + } + **/ + datepicker: { + firstDay: 0, + changeYear: true, + changeMonth: true, + showOtherMonths: true + }, + /** + Text shown as clear date button. + If <code>false</code> clear button will not be rendered. + + @property clear + @type boolean|string + @default 'x clear' + **/ + clear: '× clear' + }); + + $.fn.editabletypes.dateui = DateUI; + +}(window.jQuery)); + +/** +jQuery UI datefield input - modification for inline mode. +Shows normal <input type="text"> and binds popup datepicker. +Automatically shown in inline mode. + +@class dateuifield +@extends dateui + +@since 1.4.0 +**/ +(function ($) { + "use strict"; + + var DateUIField = function (options) { + this.init('dateuifield', options, DateUIField.defaults); + this.initPicker(options, DateUIField.defaults); + }; + + $.fn.editableutils.inherit(DateUIField, $.fn.editabletypes.dateui); + + $.extend(DateUIField.prototype, { + render: function () { + // this.$input = this.$tpl.find('input'); + this.$input.datepicker(this.options.datepicker); + $.fn.editabletypes.text.prototype.renderClear.call(this); + }, + + value2input: function(value) { + this.$input.val($.datepicker.formatDate(this.options.viewformat, value)); + }, + + input2value: function() { + return this.html2value(this.$input.val()); + }, + + activate: function() { + $.fn.editabletypes.text.prototype.activate.call(this); + }, + + toggleClear: function() { + $.fn.editabletypes.text.prototype.toggleClear.call(this); + }, + + autosubmit: function() { + //reset autosubmit to empty + } + }); + + DateUIField.defaults = $.extend({}, $.fn.editabletypes.dateui.defaults, { + /** + @property tpl + @default <input type="text"> + **/ + tpl: '<input type="text"/>', + /** + @property inputclass + @default null + **/ + inputclass: null, + + /* datepicker config */ + datepicker: { + showOn: "button", + buttonImage: "http://jqueryui.com/resources/demos/datepicker/images/calendar.gif", + buttonImageOnly: true, + firstDay: 0, + changeYear: true, + changeMonth: true, + showOtherMonths: true + }, + + /* disable clear link */ + clear: false + }); + + $.fn.editabletypes.dateuifield = DateUIField; + +}(window.jQuery)); \ No newline at end of file diff --git a/dist/jquery-editable/js/jquery-editable-poshytip.min.js b/dist/jquery-editable/js/jquery-editable-poshytip.min.js new file mode 100644 index 0000000..5612bd8 --- /dev/null +++ b/dist/jquery-editable/js/jquery-editable-poshytip.min.js @@ -0,0 +1,5 @@ +/*! X-editable - v1.4.4 +* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery +* http://github.com/vitalets/x-editable +* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ +(function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.editableform.defaults,n),this.$div=e(t),this.options.scope||(this.options.scope=this)};t.prototype={constructor:t,initInput:function(){this.input=this.options.input,this.value=this.input.str2value(this.options.value)},initTemplate:function(){this.$form=e(e.fn.editableform.template)},initButtons:function(){var t=this.$form.find(".editable-buttons");t.append(e.fn.editableform.buttons),this.options.showbuttons==="bottom"&&t.addClass("editable-buttons-bottom")},render:function(){this.$loading=e(e.fn.editableform.loading),this.$div.empty().append(this.$loading),this.initTemplate(),this.options.showbuttons?this.initButtons():this.$form.find(".editable-buttons").remove(),this.showLoading(),this.$div.triggerHandler("rendering"),this.initInput(),this.input.prerender(),this.$form.find("div.editable-input").append(this.input.$tpl),this.$div.append(this.$form),e.when(this.input.render()).then(e.proxy(function(){this.options.showbuttons||this.input.autosubmit(),this.$form.find(".editable-cancel").click(e.proxy(this.cancel,this)),this.input.error?(this.error(this.input.error),this.$form.find(".editable-submit").attr("disabled",!0),this.input.$input.attr("disabled",!0),this.$form.submit(function(e){e.preventDefault()})):(this.error(!1),this.input.$input.removeAttr("disabled"),this.$form.find(".editable-submit").removeAttr("disabled"),this.input.value2input(this.value),this.$form.submit(e.proxy(this.submit,this))),this.$div.triggerHandler("rendered"),this.showForm(),this.input.postrender&&this.input.postrender()},this))},cancel:function(){this.$div.triggerHandler("cancel")},showLoading:function(){var e,t;this.$form?(e=this.$form.outerWidth(),t=this.$form.outerHeight(),e&&this.$loading.width(e),t&&this.$loading.height(t),this.$form.hide()):(e=this.$loading.parent().width(),e&&this.$loading.width(e)),this.$loading.show()},showForm:function(e){this.$loading.hide(),this.$form.show(),e!==!1&&this.input.activate(),this.$div.triggerHandler("show")},error:function(t){var n=this.$form.find(".control-group"),r=this.$form.find(".editable-error-block"),i;if(t===!1)n.removeClass(e.fn.editableform.errorGroupClass),r.removeClass(e.fn.editableform.errorBlockClass).empty().hide();else{if(t){i=t.split("\n");for(var s=0;s<i.length;s++)i[s]=e("<div>").text(i[s]).html();t=i.join("<br>")}n.addClass(e.fn.editableform.errorGroupClass),r.addClass(e.fn.editableform.errorBlockClass).html(t).show()}},submit:function(t){t.stopPropagation(),t.preventDefault();var n,r=this.input.input2value();if(n=this.validate(r)){this.error(n),this.showForm();return}if(!this.options.savenochange&&this.input.value2str(r)==this.input.value2str(this.value)){this.$div.triggerHandler("nochange");return}e.when(this.save(r)).done(e.proxy(function(e){var t=typeof this.options.success=="function"?this.options.success.call(this.options.scope,e,r):null;if(t===!1){this.error(!1),this.showForm(!1);return}if(typeof t=="string"){this.error(t),this.showForm();return}t&&typeof t=="object"&&t.hasOwnProperty("newValue")&&(r=t.newValue),this.error(!1),this.value=r,this.$div.triggerHandler("save",{newValue:r,response:e})},this)).fail(e.proxy(function(e){var t;typeof this.options.error=="function"?t=this.options.error.call(this.options.scope,e,r):t=typeof e=="string"?e:e.responseText||e.statusText||"Unknown error!",this.error(t),this.showForm()},this))},save:function(t){var n=this.input.value2submit(t);this.options.pk=e.fn.editableutils.tryParseJson(this.options.pk,!0);var r=typeof this.options.pk=="function"?this.options.pk.call(this.options.scope):this.options.pk,i=!!(typeof this.options.url=="function"||this.options.url&&(this.options.send==="always"||this.options.send==="auto"&&r!==null&&r!==undefined)),s;if(i)return this.showLoading(),s={name:this.options.name||"",value:n,pk:r},typeof this.options.params=="function"?s=this.options.params.call(this.options.scope,s):(this.options.params=e.fn.editableutils.tryParseJson(this.options.params,!0),e.extend(s,this.options.params)),typeof this.options.url=="function"?this.options.url.call(this.options.scope,s):e.ajax(e.extend({url:this.options.url,data:s,type:"POST"},this.options.ajaxOptions))},validate:function(e){e===undefined&&(e=this.value);if(typeof this.options.validate=="function")return this.options.validate.call(this.options.scope,e)},option:function(e,t){e in this.options&&(this.options[e]=t),e==="value"&&this.setValue(t)},setValue:function(e,t){t?this.value=this.input.str2value(e):this.value=e,this.$form&&this.$form.is(":visible")&&this.input.value2input(this.value)}},e.fn.editableform=function(n){var r=arguments;return this.each(function(){var i=e(this),s=i.data("editableform"),o=typeof n=="object"&&n;s||i.data("editableform",s=new t(this,o)),typeof n=="string"&&s[n].apply(s,Array.prototype.slice.call(r,1))})},e.fn.editableform.Constructor=t,e.fn.editableform.defaults={type:"text",url:null,params:null,name:null,pk:null,value:null,send:"auto",validate:null,success:null,error:null,ajaxOptions:null,showbuttons:!0,scope:null,savenochange:!1},e.fn.editableform.template='<form class="form-inline editableform"><div class="control-group"><div><div class="editable-input"></div><div class="editable-buttons"></div></div><div class="editable-error-block"></div></div></form>',e.fn.editableform.loading='<div class="editableform-loading"></div>',e.fn.editableform.buttons='<button type="submit" class="editable-submit">ok</button><button type="button" class="editable-cancel">cancel</button>',e.fn.editableform.errorGroupClass=null,e.fn.editableform.errorBlockClass="editable-error"})(window.jQuery),function(e){"use strict";e.fn.editableutils={inherit:function(e,t){var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e,e.superclass=t.prototype},setCursorPosition:function(e,t){if(e.setSelectionRange)e.setSelectionRange(t,t);else if(e.createTextRange){var n=e.createTextRange();n.collapse(!0),n.moveEnd("character",t),n.moveStart("character",t),n.select()}},tryParseJson:function(e,t){if(typeof e=="string"&&e.length&&e.match(/^[\{\[].*[\}\]]$/))if(t)try{e=(new Function("return "+e))()}catch(n){}finally{return e}else e=(new Function("return "+e))();return e},sliceObj:function(t,n,r){var i,s,o={};if(!e.isArray(n)||!n.length)return o;for(var u=0;u<n.length;u++){i=n[u],t.hasOwnProperty(i)&&(o[i]=t[i]);if(r===!0)continue;s=i.toLowerCase(),t.hasOwnProperty(s)&&(o[i]=t[s])}return o},getConfigData:function(t){var n={};return e.each(t.data(),function(e,t){if(typeof t!="object"||t&&typeof t=="object"&&(t.constructor===Object||t.constructor===Array))n[e]=t}),n},objectKeys:function(e){if(Object.keys)return Object.keys(e);if(e!==Object(e))throw new TypeError("Object.keys called on a non-object");var t=[],n;for(n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t},escape:function(t){return e("<div>").text(t).html()},itemsByValue:function(t,n,r){if(!n||t===null)return[];r=r||"value";var i=e.isArray(t),s=[],o=this;return e.each(n,function(n,u){u.children?s=s.concat(o.itemsByValue(t,u.children,r)):i?e.grep(t,function(e){return e==(u&&typeof u==="object"?u[r]:u)}).length&&s.push(u):t==(u&&typeof u==="object"?u[r]:u)&&s.push(u)}),s},createInput:function(t){var n,r,i,s=t.type;return s==="date"&&(t.mode==="inline"?e.fn.editabletypes.datefield?s="datefield":e.fn.editabletypes.dateuifield&&(s="dateuifield"):e.fn.editabletypes.date?s="date":e.fn.editabletypes.dateui&&(s="dateui"),s==="date"&&!e.fn.editabletypes.date&&(s="combodate")),s==="datetime"&&t.mode==="inline"&&(s="datetimefield"),s==="wysihtml5"&&!e.fn.editabletypes[s]&&(s="textarea"),typeof e.fn.editabletypes[s]=="function"?(n=e.fn.editabletypes[s],r=this.sliceObj(t,this.objectKeys(n.defaults)),i=new n(r),i):(e.error("Unknown type: "+s),!1)}}}(window.jQuery),function(e){"use strict";var t=function(e,t){this.init(e,t)},n=function(e,t){this.init(e,t)};t.prototype={containerName:null,innerCss:null,containerClass:"editable-container editable-popup",init:function(n,r){this.$element=e(n),this.options=e.extend({},e.fn.editableContainer.defaults,r),this.splitOptions(),this.formOptions.scope=this.$element[0],this.initContainer(),this.$element.on("destroyed",e.proxy(function(){this.destroy()},this)),e(document).data("editable-handlers-attached")||(e(document).on("keyup.editable",function(t){t.which===27&&e(".editable-open").editableContainer("hide")}),e(document).on("click.editable",function(n){var r=e(n.target),i,s=[".editable-container",".ui-datepicker-header",".datepicker",".modal-backdrop",".bootstrap-wysihtml5-insert-image-modal",".bootstrap-wysihtml5-insert-link-modal"];if(!e.contains(document.documentElement,n.target))return;if(r.is(document))return;for(i=0;i<s.length;i++)if(r.is(s[i])||r.parents(s[i]).length)return;t.prototype.closeOthers(n.target)}),e(document).data("editable-handlers-attached",!0))},splitOptions:function(){this.containerOptions={},this.formOptions={};if(!e.fn[this.containerName])throw new Error(this.containerName+" not found. Have you included corresponding js file?");var t=e.fn[this.containerName].defaults;for(var n in this.options)n in t?this.containerOptions[n]=this.options[n]:this.formOptions[n]=this.options[n]},tip:function(){return this.container()?this.container().$tip:null},container:function(){return this.$element.data(this.containerDataName||this.containerName)},call:function(){this.$element[this.containerName].apply(this.$element,arguments)},initContainer:function(){this.call(this.containerOptions)},renderForm:function(){this.$form.editableform(this.formOptions).on({save:e.proxy(this.save,this),nochange:e.proxy(function(){this.hide("nochange")},this),cancel:e.proxy(function(){this.hide("cancel")},this),show:e.proxy(this.setPosition,this),rendering:e.proxy(this.setPosition,this),resize:e.proxy(this.setPosition,this),rendered:e.proxy(function(){this.$element.triggerHandler("shown",this)},this)}).editableform("render")},show:function(t){this.$element.addClass("editable-open"),t!==!1&&this.closeOthers(this.$element[0]),this.innerShow(),this.tip().addClass(this.containerClass),this.$form,this.$form=e("<div>"),this.tip().is(this.innerCss)?this.tip().append(this.$form):this.tip().find(this.innerCss).append(this.$form),this.renderForm()},hide:function(e){if(!this.tip()||!this.tip().is(":visible")||!this.$element.hasClass("editable-open"))return;this.$element.removeClass("editable-open"),this.innerHide(),this.$element.triggerHandler("hidden",e||"manual")},innerShow:function(){},innerHide:function(){},toggle:function(e){this.container()&&this.tip()&&this.tip().is(":visible")?this.hide():this.show(e)},setPosition:function(){},save:function(e,t){this.$element.triggerHandler("save",t),this.hide("save")},option:function(e,t){this.options[e]=t,e in this.containerOptions?(this.containerOptions[e]=t,this.setContainerOption(e,t)):(this.formOptions[e]=t,this.$form&&this.$form.editableform("option",e,t))},setContainerOption:function(e,t){this.call("option",e,t)},destroy:function(){this.hide(),this.innerDestroy(),this.$element.off("destroyed"),this.$element.removeData("editableContainer")},innerDestroy:function(){},closeOthers:function(t){e(".editable-open").each(function(n,r){if(r===t||e(r).find(t).length)return;var i=e(r),s=i.data("editableContainer");if(!s)return;s.options.onblur==="cancel"?i.data("editableContainer").hide("onblur"):s.options.onblur==="submit"&&i.data("editableContainer").tip().find("form").submit()})},activate:function(){this.tip&&this.tip().is(":visible")&&this.$form&&this.$form.data("editableform").input.activate()}},e.fn.editableContainer=function(r){var i=arguments;return this.each(function(){var s=e(this),o="editableContainer",u=s.data(o),a=typeof r=="object"&&r,f=a.mode==="inline"?n:t;u||s.data(o,u=new f(this,a)),typeof r=="string"&&u[r].apply(u,Array.prototype.slice.call(i,1))})},e.fn.editableContainer.Popup=t,e.fn.editableContainer.Inline=n,e.fn.editableContainer.defaults={value:null,placement:"top",autohide:!0,onblur:"cancel",anim:!1,mode:"popup"},jQuery.event.special.destroyed={remove:function(e){e.handler&&e.handler()}}}(window.jQuery),function(e){"use strict";e.extend(e.fn.editableContainer.Inline.prototype,e.fn.editableContainer.Popup.prototype,{containerName:"editableform",innerCss:".editable-inline",containerClass:"editable-container editable-inline",initContainer:function(){this.$tip=e("<span></span>"),this.options.anim||(this.options.anim=0)},splitOptions:function(){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,e.proxy(function(){this.$element.show(),this.innerDestroy()},this))},innerDestroy:function(){this.tip()&&this.tip().empty().remove()}})}(window.jQuery),function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.editable.defaults,n,e.fn.editableutils.getConfigData(this.$element)),this.options.selector?this.initLive():this.init()};t.prototype={constructor:t,init:function(){var t=!1,n,r;this.options.name=this.options.name||this.$element.attr("id"),this.options.scope=this.$element[0],this.input=e.fn.editableutils.createInput(this.options);if(!this.input)return;this.options.value===undefined||this.options.value===null?(this.value=this.input.html2value(e.trim(this.$element.html())),t=!0):(this.options.value=e.fn.editableutils.tryParseJson(this.options.value,!0),typeof this.options.value=="string"?this.value=this.input.str2value(this.options.value):this.value=this.options.value),this.$element.addClass("editable"),this.options.toggle!=="manual"?(this.$element.addClass("editable-click"),this.$element.on(this.options.toggle+".editable",e.proxy(function(e){e.preventDefault();if(this.options.toggle==="mouseenter")this.show();else{var t=this.options.toggle!=="click";this.toggle(t)}},this))):this.$element.attr("tabindex",-1);switch(this.options.autotext){case"always":n=!0;break;case"auto":n=!e.trim(this.$element.text()).length&&this.value!==null&&this.value!==undefined&&!t;break;default:n=!1}e.when(n?this.render():!0).then(e.proxy(function(){this.options.disabled?this.disable():this.enable(),this.$element.triggerHandler("init",this)},this))},initLive:function(){var t=this.options.selector;this.options.selector=!1,this.options.autotext="never",this.$element.on(this.options.toggle+".editable",t,e.proxy(function(t){var n=e(t.target);n.data("editable")||(n.hasClass(this.options.emptyclass)&&n.empty(),n.editable(this.options).trigger(t))},this))},render:function(e){if(this.options.display===!1)return;return this.input.value2htmlFinal?this.input.value2html(this.value,this.$element[0],this.options.display,e):typeof this.options.display=="function"?this.options.display.call(this.$element[0],this.value,e):this.input.value2html(this.value,this.$element[0])},enable:function(){this.options.disabled=!1,this.$element.removeClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.options.toggle!=="manual"&&this.$element.attr("tabindex")==="-1"&&this.$element.removeAttr("tabindex")},disable:function(){this.options.disabled=!0,this.hide(),this.$element.addClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.$element.attr("tabindex",-1)},toggleDisabled:function(){this.options.disabled?this.enable():this.disable()},option:function(t,n){if(t&&typeof t=="object"){e.each(t,e.proxy(function(t,n){this.option(e.trim(t),n)},this));return}this.options[t]=n;if(t==="disabled")return n?this.disable():this.enable();t==="value"&&this.setValue(n),this.container&&this.container.option(t,n),this.input.option&&this.input.option(t,n)},handleEmpty:function(t){if(this.options.display===!1)return;this.isEmpty=t!==undefined?t:e.trim(this.$element.text())==="",this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.text(this.options.emptytext),this.options.emptyclass&&this.$element.addClass(this.options.emptyclass)):this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)},show:function(t){if(this.options.disabled)return;if(!this.container){var n=e.extend({},this.options,{value:this.value,input:this.input});this.$element.editableContainer(n),this.$element.on("save.internal",e.proxy(this.save,this)),this.container=this.$element.data("editableContainer")}else if(this.container.tip().is(":visible"))return;this.container.show(t)},hide:function(){this.container&&this.container.hide()},toggle:function(e){this.container&&this.container.tip().is(":visible")?this.hide():this.show(e)},save:function(e,t){if(this.options.unsavedclass){var n=!1;n=n||typeof this.options.url=="function",n=n||this.options.display===!1,n=n||t.response!==undefined,n=n||this.options.savenochange&&this.input.value2str(this.value)!==this.input.value2str(t.newValue),n?this.$element.removeClass(this.options.unsavedclass):this.$element.addClass(this.options.unsavedclass)}this.setValue(t.newValue,!1,t.response)},validate:function(){if(typeof this.options.validate=="function")return this.options.validate.call(this,this.value)},setValue:function(t,n,r){n?this.value=this.input.str2value(t):this.value=t,this.container&&this.container.option("value",this.value),e.when(this.render(r)).then(e.proxy(function(){this.handleEmpty()},this))},activate:function(){this.container&&this.container.activate()},destroy:function(){this.disable(),this.container&&this.container.destroy(),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")}},e.fn.editable=function(n){var r={},i=arguments,s="editable";switch(n){case"validate":return this.each(function(){var t=e(this),n=t.data(s),i;n&&(i=n.validate())&&(r[n.options.name]=i)}),r;case"getValue":return this.each(function(){var t=e(this),n=t.data(s);n&&n.value!==undefined&&n.value!==null&&(r[n.options.name]=n.input.value2submit(n.value))}),r;case"submit":var o=arguments[1]||{},u=this,a=this.editable("validate"),f;return e.isEmptyObject(a)?(f=this.editable("getValue"),o.data&&e.extend(f,o.data),e.ajax(e.extend({url:o.url,data:f,type:"POST"},o.ajaxOptions)).success(function(e){typeof o.success=="function"&&o.success.call(u,e,o)}).error(function(){typeof o.error=="function"&&o.error.apply(u,arguments)})):typeof o.error=="function"&&o.error.call(u,a),this}return this.each(function(){var r=e(this),o=r.data(s),u=typeof n=="object"&&n;o||r.data(s,o=new t(this,u)),typeof n=="string"&&o[n].apply(o,Array.prototype.slice.call(i,1))})},e.fn.editable.defaults={type:"text",disabled:!1,toggle:"click",emptytext:"Empty",autotext:"auto",value:null,display:null,emptyclass:"editable-empty",unsavedclass:"editable-unsaved",selector:null}}(window.jQuery),function(e){"use strict";e.fn.editabletypes={};var t=function(){};t.prototype={init:function(t,n,r){this.type=t,this.options=e.extend({},r,n)},prerender:function(){this.$tpl=e(this.options.tpl),this.$input=this.$tpl,this.$clear=null,this.error=null},render:function(){},value2html:function(t,n){e(n).text(t)},html2value:function(t){return e("<div>").html(t).text()},value2str:function(e){return e},str2value:function(e){return e},value2submit:function(e){return e},value2input:function(e){this.$input.val(e)},input2value:function(){return this.$input.val()},activate:function(){this.$input.is(":visible")&&this.$input.focus()},clear:function(){this.$input.val(null)},escape:function(t){return e("<div>").text(t).html()},autosubmit:function(){},setClass:function(){this.options.inputclass&&this.$input.addClass(this.options.inputclass)},setAttr:function(e){this.options[e]!==undefined&&this.options[e]!==null&&this.$input.attr(e,this.options[e])},option:function(e,t){this.options[e]=t}},t.defaults={tpl:"",inputclass:"input-medium",scope:null,showbuttons:!0},e.extend(e.fn.editabletypes,{abstractinput:t})}(window.jQuery),function(e){"use strict";var t=function(e){};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){var t=e.Deferred();return this.error=null,this.onSourceReady(function(){this.renderList(),t.resolve()},function(){this.error=this.options.sourceError,t.resolve()}),t.promise()},html2value:function(e){return null},value2html:function(t,n,r,i){var s=e.Deferred(),o=function(){typeof r=="function"?r.call(n,t,this.sourceData,i):this.value2htmlFinal(t,n),s.resolve()};return t===null?o.call(this):this.onSourceReady(o,function(){s.resolve()}),s.promise()},onSourceReady:function(t,n){if(e.isArray(this.sourceData)){t.call(this);return}try{this.options.source=e.fn.editableutils.tryParseJson(this.options.source,!1)}catch(r){n.call(this);return}var i=this.options.source;e.isFunction(i)&&(i=i.call(this.options.scope));if(typeof i=="string"){if(this.options.sourceCache){var s=i,o;e(document).data(s)||e(document).data(s,{}),o=e(document).data(s);if(o.loading===!1&&o.sourceData){this.sourceData=o.sourceData,this.doPrepend(),t.call(this);return}if(o.loading===!0){o.callbacks.push(e.proxy(function(){this.sourceData=o.sourceData,this.doPrepend(),t.call(this)},this)),o.err_callbacks.push(e.proxy(n,this));return}o.loading=!0,o.callbacks=[],o.err_callbacks=[]}e.ajax({url:i,type:"get",cache:!1,dataType:"json",success:e.proxy(function(r){o&&(o.loading=!1),this.sourceData=this.makeArray(r),e.isArray(this.sourceData)?(o&&(o.sourceData=this.sourceData,e.each(o.callbacks,function(){this.call()})),this.doPrepend(),t.call(this)):(n.call(this),o&&e.each(o.err_callbacks,function(){this.call()}))},this),error:e.proxy(function(){n.call(this),o&&(o.loading=!1,e.each(o.err_callbacks,function(){this.call()}))},this)})}else this.sourceData=this.makeArray(i),e.isArray(this.sourceData)?(this.doPrepend(),t.call(this)):n.call(this)},doPrepend:function(){if(this.options.prepend===null||this.options.prepend===undefined)return;e.isArray(this.prependData)||(e.isFunction(this.options.prepend)&&(this.options.prepend=this.options.prepend.call(this.options.scope)),this.options.prepend=e.fn.editableutils.tryParseJson(this.options.prepend,!0),typeof this.options.prepend=="string"&&(this.options.prepend={"":this.options.prepend}),this.prependData=this.makeArray(this.options.prepend)),e.isArray(this.prependData)&&e.isArray(this.sourceData)&&(this.sourceData=this.prependData.concat(this.sourceData))},renderList:function(){},value2htmlFinal:function(e,t){},makeArray:function(t){var n,r,i=[],s,o;if(!t||typeof t=="string")return null;if(e.isArray(t)){o=function(e,t){r={value:e,text:t};if(n++>=2)return!1};for(var u=0;u<t.length;u++)s=t[u],typeof s=="object"?(n=0,e.each(s,o),n===1?i.push(r):n>1&&(s.children&&(s.children=this.makeArray(s.children)),i.push(s))):i.push({value:s,text:s})}else e.each(t,function(e,t){i.push({value:e,text:t})});return i},option:function(e,t){this.options[e]=t,e==="source"&&(this.sourceData=null),e==="prepend"&&(this.prependData=null)}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{source:null,prepend:!1,sourceError:"Error when loading list",sourceCache:!0}),e.fn.editabletypes.list=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("text",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.renderClear(),this.setClass(),this.setAttr("placeholder")},activate:function(){this.$input.is(":visible")&&(this.$input.focus(),e.fn.editableutils.setCursorPosition(this.$input.get(0),this.$input.val().length),this.toggleClear&&this.toggleClear())},renderClear:function(){this.options.clear&&(this.$clear=e('<span class="editable-clear-x"></span>'),this.$input.after(this.$clear).css("padding-right",24).keyup(e.proxy(function(t){if(~e.inArray(t.keyCode,[40,38,9,13,27]))return;clearTimeout(this.t);var n=this;this.t=setTimeout(function(){n.toggleClear(t)},100)},this)).parent().css("position","relative"),this.$clear.click(e.proxy(this.clear,this)))},postrender:function(){},toggleClear:function(e){if(!this.$clear)return;var t=this.$input.val().length,n=this.$clear.is(":visible");t&&!n&&this.$clear.show(),!t&&n&&this.$clear.hide()},clear:function(){this.$clear.hide(),this.$input.val("").focus()}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',placeholder:null,clear:!0}),e.fn.editabletypes.text=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("textarea",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.setAttr("placeholder"),this.setAttr("rows"),this.$input.keydown(function(t){t.ctrlKey&&t.which===13&&e(this).closest("form").submit()})},value2html:function(t,n){var r="",i;if(t){i=t.split("\n");for(var s=0;s<i.length;s++)i[s]=e("<div>").text(i[s]).html();r=i.join("<br>")}e(n).html(r)},html2value:function(t){if(!t)return"";var n=new RegExp(String.fromCharCode(10),"g"),r=t.split(/<br\s*\/?>/i);for(var i=0;i<r.length;i++){var s=e("<div>").html(r[i]).text();s=s.replace(n,""),r[i]=s}return r.join("\n")},activate:function(){e.fn.editabletypes.text.prototype.activate.call(this)}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:"<textarea></textarea>",inputclass:"input-large",placeholder:null,rows:7}),e.fn.editabletypes.textarea=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("select",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.list),e.extend(t.prototype,{renderList:function(){this.$input.empty();var t=function(n,r){if(e.isArray(r))for(var i=0;i<r.length;i++)r[i].children?n.append(t(e("<optgroup>",{label:r[i].text}),r[i].children)):n.append(e("<option>",{value:r[i].value}).text(r[i].text));return n};t(this.$input,this.sourceData),this.setClass(),this.$input.on("keydown.editable",function(t){t.which===13&&e(this).closest("form").submit()})},value2htmlFinal:function(t,n){var r="",i=e.fn.editableutils.itemsByValue(t,this.sourceData);i.length&&(r=i[0].text),e(n).text(r)},autosubmit:function(){this.$input.off("keydown.editable").on("change.editable",function(){e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.list.defaults,{tpl:"<select></select>"}),e.fn.editabletypes.select=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("checklist",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.list),e.extend(t.prototype,{renderList:function(){var t,n;this.$tpl.empty();if(!e.isArray(this.sourceData))return;for(var r=0;r<this.sourceData.length;r++)t=e("<label>").append(e("<input>",{type:"checkbox",value:this.sourceData[r].value})).append(e("<span>").text(" "+this.sourceData[r].text)),e("<div>").append(t).appendTo(this.$tpl);this.$input=this.$tpl.find('input[type="checkbox"]'),this.setClass()},value2str:function(t){return e.isArray(t)?t.sort().join(e.trim(this.options.separator)):""},str2value:function(t){var n,r=null;return typeof t=="string"&&t.length?(n=new RegExp("\\s*"+e.trim(this.options.separator)+"\\s*"),r=t.split(n)):e.isArray(t)?r=t:r=[t],r},value2input:function(t){this.$input.prop("checked",!1),e.isArray(t)&&t.length&&this.$input.each(function(n,r){var i=e(r);e.each(t,function(e,t){i.val()==t&&i.prop("checked",!0)})})},input2value:function(){var t=[];return this.$input.filter(":checked").each(function(n,r){t.push(e(r).val())}),t},value2htmlFinal:function(t,n){var r=[],i=e.fn.editableutils.itemsByValue(t,this.sourceData);i.length?(e.each(i,function(t,n){r.push(e.fn.editableutils.escape(n.text))}),e(n).html(r.join("<br>"))):e(n).empty()},activate:function(){this.$input.first().focus()},autosubmit:function(){this.$input.on("keydown",function(t){t.which===13&&e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.list.defaults,{tpl:'<div class="editable-checklist"></div>',inputclass:null,separator:","}),e.fn.editabletypes.checklist=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("password",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),e.extend(t.prototype,{value2html:function(t,n){t?e(n).text("[hidden]"):e(n).empty()},html2value:function(e){return null}}),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="password">'}),e.fn.editabletypes.password=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("email",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="email">'}),e.fn.editabletypes.email=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("url",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="url">'}),e.fn.editabletypes.url=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("tel",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="tel">'}),e.fn.editabletypes.tel=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("number",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),e.extend(t.prototype,{render:function(){t.superclass.render.call(this),this.setAttr("min"),this.setAttr("max"),this.setAttr("step")},postrender:function(){this.$clear&&this.$clear.css({right:24})}}),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="number">',inputclass:"input-mini",min:null,max:null,step:null}),e.fn.editabletypes.number=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("range",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.number),e.extend(t.prototype,{render:function(){this.$input=this.$tpl.filter("input"),this.setClass(),this.setAttr("min"),this.setAttr("max"),this.setAttr("step"),this.$input.on("input",function(){e(this).siblings("output").text(e(this).val())})},activate:function(){this.$input.focus()}}),t.defaults=e.extend({},e.fn.editabletypes.number.defaults,{tpl:'<input type="range"><output style="width: 30px; display: inline-block"></output>',inputclass:"input-medium"}),e.fn.editabletypes.range=t}(window.jQuery),function(e){"use strict";var t=function(n){this.init("select2",n,t.defaults),n.select2=n.select2||{};var r=this,i={placeholder:n.placeholder};this.isMultiple=n.select2.tags||n.select2.multiple,n.select2.tags||(n.source&&(i.data=n.source),i.initSelection=function(t,n){var s=r.str2value(t.val()),o=e.fn.editableutils.itemsByValue(s,i.data,"id");e.isArray(o)&&o.length&&!r.isMultiple&&(o=o[0]),n(o)}),this.options.select2=e.extend({},t.defaults.select2,i,n.select2)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.$input.select2(this.options.select2),"ajax"in this.options.select2,this.isMultiple&&this.$input.on("change",function(){e(this).closest("form").parent().triggerHandler("resize")})},value2html:function(t,n){var r="",i;this.$input?i=this.$input.select2("data"):this.options.select2.tags?i=t:this.options.select2.data&&(i=e.fn.editableutils.itemsByValue(t,this.options.select2.data,"id")),e.isArray(i)?(r=[],e.each(i,function(e,t){r.push(t&&typeof t=="object"?t.text:t)})):i&&(r=i.text),r=e.isArray(r)?r.join(this.options.viewseparator):r,e(n).text(r)},html2value:function(e){return this.options.select2.tags?this.str2value(e,this.options.viewseparator):null},value2input:function(e){this.$input.val(e).trigger("change",!0)},input2value:function(){return this.$input.select2("val")},str2value:function(t,n){if(typeof t!="string"||!this.isMultiple)return t;n=n||this.options.select2.separator||e.fn.select2.defaults.separator;var r,i,s;if(t===null||t.length<1)return null;r=t.split(n);for(i=0,s=r.length;i<s;i+=1)r[i]=e.trim(r[i]);return r},autosubmit:function(){this.$input.on("change",function(t,n){n||e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="hidden">',select2:null,placeholder:null,source:null,viewseparator:", "}),e.fn.editabletypes.select2=t}(window.jQuery),function(e){var t=function(t,n){this.$element=e(t);if(!this.$element.is("input")){e.error("Combodate should be applied to INPUT element");return}this.options=e.extend({},e.fn.combodate.defaults,n,this.$element.data()),this.init()};t.prototype={constructor:t,init:function(){this.map={day:["D","date"],month:["M","month"],year:["Y","year"],hour:["[Hh]","hours"],minute:["m","minutes"],second:["s","seconds"],ampm:["[Aa]",""]},this.$widget=e('<span class="combodate"></span>').html(this.getTemplate()),this.initCombos(),this.$widget.on("change","select",e.proxy(function(){this.$element.val(this.getValue())},this)),this.$widget.find("select").css("width","auto"),this.$element.hide().after(this.$widget),this.setValue(this.$element.val()||this.options.value)},getTemplate:function(){var t=this.options.template;return e.each(this.map,function(e,n){n=n[0];var r=new RegExp(n+"+"),i=n.length>1?n.substring(1,2):n;t=t.replace(r,"{"+i+"}")}),t=t.replace(/ /g," "),e.each(this.map,function(e,n){n=n[0];var r=n.length>1?n.substring(1,2):n;t=t.replace("{"+r+"}",'<select class="'+e+'"></select>')}),t},initCombos:function(){var t=this;e.each(this.map,function(e,n){var r=t.$widget.find("."+e),i,s;r.length&&(t["$"+e]=r,i="fill"+e.charAt(0).toUpperCase()+e.slice(1),s=t[i](),t["$"+e].html(t.renderItems(s)))})},initItems:function(e){var t=[],n;if(this.options.firstItem==="name"){n=moment.relativeTime||moment.langData()._relativeTime;var r=typeof n[e]=="function"?n[e](1,!0,e,!1):n[e];r=r.split(" ").reverse()[0],t.push(["",r])}else this.options.firstItem==="empty"&&t.push(["",""]);return t},renderItems:function(e){var t=[];for(var n=0;n<e.length;n++)t.push('<option value="'+e[n][0]+'">'+e[n][1]+"</option>");return t.join("\n")},fillDay:function(){var e=this.initItems("d"),t,n,r=this.options.template.indexOf("DD")!==-1;for(n=1;n<=31;n++)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillMonth:function(){var e=this.initItems("M"),t,n,r=this.options.template.indexOf("MMMM")!==-1,i=this.options.template.indexOf("MMM")!==-1,s=this.options.template.indexOf("MM")!==-1;for(n=0;n<=11;n++)r?t=moment().month(n).format("MMMM"):i?t=moment().month(n).format("MMM"):s?t=this.leadZero(n+1):t=n+1,e.push([n,t]);return e},fillYear:function(){var e=[],t,n,r=this.options.template.indexOf("YYYY")!==-1;for(n=this.options.maxYear;n>=this.options.minYear;n--)t=r?n:(n+"").substring(2),e[this.options.yearDescending?"push":"unshift"]([n,t]);return e=this.initItems("y").concat(e),e},fillHour:function(){var e=this.initItems("h"),t,n,r=this.options.template.indexOf("h")!==-1,i=this.options.template.indexOf("H")!==-1,s=this.options.template.toLowerCase().indexOf("hh")!==-1,o=r?12:23;for(n=0;n<=o;n++)t=s?this.leadZero(n):n,e.push([n,t]);return e},fillMinute:function(){var e=this.initItems("m"),t,n,r=this.options.template.indexOf("mm")!==-1;for(n=0;n<=59;n+=this.options.minuteStep)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillSecond:function(){var e=this.initItems("s"),t,n,r=this.options.template.indexOf("ss")!==-1;for(n=0;n<=59;n+=this.options.secondStep)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillAmpm:function(){var e=this.options.template.indexOf("a")!==-1,t=this.options.template.indexOf("A")!==-1,n=[["am",e?"am":"AM"],["pm",e?"pm":"PM"]];return n},getValue:function(t){var n,r={},i=this,s=!1;return e.each(this.map,function(e,t){if(e==="ampm")return;var n=e==="day"?1:0;r[e]=i["$"+e]?parseInt(i["$"+e].val(),10):n;if(isNaN(r[e]))return s=!0,!1}),s?"":(this.$ampm&&(r.hour=this.$ampm.val()==="am"?r.hour:r.hour+12,r.hour===24&&(r.hour=0)),n=moment([r.year,r.month,r.day,r.hour,r.minute,r.second]),this.highlight(n),t=t===undefined?this.options.format:t,t===null?n.isValid()?n:null:n.isValid()?n.format(t):"")},setValue:function(t){function s(t,n){var r={};return t.children("option").each(function(t,i){var s=e(i).attr("value"),o;if(s==="")return;o=Math.abs(s-n);if(typeof r.distance=="undefined"||o<r.distance)r={value:s,distance:o}}),r.value}if(!t)return;var n=typeof t=="string"?moment(t,this.options.format):moment(t),r=this,i={};n.isValid()&&(e.each(this.map,function(e,t){if(e==="ampm")return;i[e]=n[t[1]]()}),this.$ampm&&(i.hour>12?(i.hour-=12,i.ampm="pm"):i.ampm="am"),e.each(i,function(e,t){r["$"+e]&&(e==="minute"&&r.options.minuteStep>1&&r.options.roundTime&&(t=s(r["$"+e],t)),e==="second"&&r.options.secondStep>1&&r.options.roundTime&&(t=s(r["$"+e],t)),r["$"+e].val(t))}),this.$element.val(n.format(this.options.format)))},highlight:function(e){e.isValid()?this.options.errorClass?this.$widget.removeClass(this.options.errorClass):this.$widget.find("select").css("border-color",this.borderColor):this.options.errorClass?this.$widget.addClass(this.options.errorClass):(this.borderColor||(this.borderColor=this.$widget.find("select").css("border-color")),this.$widget.find("select").css("border-color","red"))},leadZero:function(e){return e<=9?"0"+e:e},destroy:function(){this.$widget.remove(),this.$element.removeData("combodate").show()}},e.fn.combodate=function(n){var r,i=Array.apply(null,arguments);return i.shift(),n==="getValue"&&this.length&&(r=this.eq(0).data("combodate"))?r.getValue.apply(r,i):this.each(function(){var r=e(this),s=r.data("combodate"),o=typeof n=="object"&&n;s||r.data("combodate",s=new t(this,o)),typeof n=="string"&&typeof s[n]=="function"&&s[n].apply(s,i)})},e.fn.combodate.defaults={format:"DD-MM-YYYY HH:mm",template:"D / MMM / YYYY H : mm",value:null,minYear:1970,maxYear:2015,yearDescending:!0,minuteStep:5,secondStep:1,firstItem:"empty",errorClass:null,roundTime:!0}}(window.jQuery),function(e){"use strict";var t=function(n){this.init("combodate",n,t.defaults),this.options.viewformat||(this.options.viewformat=this.options.format),n.combodate=e.fn.editableutils.tryParseJson(n.combodate,!0),this.options.combodate=e.extend({},t.defaults.combodate,n.combodate,{format:this.options.format,template:this.options.template})};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.$input.combodate(this.options.combodate)},value2html:function(t,n){var r=t?t.format(this.options.viewformat):"";e(n).text(r)},html2value:function(e){return e?moment(e,this.options.viewformat):null},value2str:function(e){return e?e.format(this.options.format):""},str2value:function(e){return e?moment(e,this.options.format):null},value2submit:function(e){return this.value2str(e)},value2input:function(e){this.$input.combodate("setValue",e)},input2value:function(){return this.$input.combodate("getValue",null)},activate:function(){this.$input.siblings(".combodate").find("select").eq(0).focus()},autosubmit:function(){}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',inputclass:null,format:"YYYY-MM-DD",viewformat:null,template:"D / MMM / YYYY",combodate:null}),e.fn.editabletypes.combodate=t}(window.jQuery),function(e){"use strict";e.extend(e.fn.editableContainer.Popup.prototype,{containerName:"poshytip",innerCss:"div.tip-inner",initContainer:function(){this.handlePlacement(),e.extend(this.containerOptions,{showOn:"none",content:"",alignTo:"target"}),this.call(this.containerOptions)},show:function(t){this.$element.addClass("editable-open"),t!==!1&&this.closeOthers(this.$element[0]),this.$form=e("<div>"),this.renderForm();var n=e("<label>").text(this.options.title||this.$element.data("title")||this.$element.data("originalTitle")),r=e("<div>").append(n).append(this.$form);this.call("update",r),this.call("show"),this.tip().addClass("editable-container"),this.$form.data("editableform").input.activate()},innerHide:function(){this.call("hide")},innerDestroy:function(){this.call("destroy")},setPosition:function(){this.container().refresh(!1)},handlePlacement:function(){var t,n,r=0,i=0;switch(this.options.placement){case"top":t="center",n="top",i=5;break;case"right":t="right",n="center",r=10;break;case"bottom":t="center",n="bottom",i=5;break;case"left":t="left",n="center",r=10}e.extend(this.containerOptions,{alignX:t,offsetX:r,alignY:n,offsetY:i})}}),e.fn.editableContainer.defaults=e.extend({},e.fn.editableContainer.defaults,{className:"tip-yellowsimple"});if(e.Poshytip){var t=[],n=/^url\(["']?([^"'\)]*)["']?\);?$/i,r=/\.png$/i,i=!!window.createPopup&&document.documentElement.currentStyle.minWidth=="undefined";e.Poshytip.prototype.refresh=function(t){if(this.disabled)return;var s;if(t){if(!this.$tip.data("active"))return;s={left:this.$tip.css("left"),top:this.$tip.css("top")}}this.$tip.css({left:0,top:0}).appendTo(document.body),this.opacity===undefined&&(this.opacity=this.$tip.css("opacity"));var o=this.$tip.css("background-image").match(n),u=this.$arrow.css("background-image").match(n);if(o){var a=r.test(o[1]);i&&a?(this.$tip.css("background-image","none"),this.$inner.css({margin:0,border:0,padding:0}),o=a=!1):this.$tip.prepend('<table class="fallback" border="0" cellpadding="0" cellspacing="0"><tr><td class="tip-top tip-bg-image" colspan="2"><span></span></td><td class="tip-right tip-bg-image" rowspan="2"><span></span></td></tr><tr><td class="tip-left tip-bg-image" rowspan="2"><span></span></td><td></td></tr><tr><td class="tip-bottom tip-bg-image" colspan="2"><span></span></td></tr></table>').css({border:0,padding:0,"background-image":"none","background-color":"transparent"}).find(".tip-bg-image").css("background-image",'url("'+o[1]+'")').end().find("td").eq(3).append(this.$inner),a&&!e.support.opacity&&(this.opts.fade=!1)}u&&!e.support.opacity&&(i&&r.test(u[1])&&(u=!1,this.$arrow.css("background-image","none")),this.opts.fade=!1);var f=this.$tip.find("table.fallback");if(i){this.$tip[0].style.width="",f.width("auto").find("td").eq(3).width("auto");var l=this.$tip.width(),c=parseInt(this.$tip.css("min-width"),10),h=parseInt(this.$tip.css("max-width"),10);!isNaN(c)&&l<c?l=c:!isNaN(h)&&l>h&&(l=h),this.$tip.add(f).width(l).eq(0).find("td").eq(3).width("100%")}else f[0]&&f.width("auto").find("td").eq(3).width("auto").end().end().width(document.defaultView&&document.defaultView.getComputedStyle&&parseFloat(document.defaultView.getComputedStyle(this.$tip[0],null).width)||this.$tip.width()).find("td").eq(3).width("100%");this.tipOuterW=this.$tip.outerWidth(),this.tipOuterH=this.$tip.outerHeight(),this.calcPos(),u&&this.pos.arrow&&(this.$arrow[0].className="tip-arrow tip-arrow-"+this.pos.arrow,this.$arrow.css("visibility","inherit"));if(t){this.asyncAnimating=!0;var p=this;this.$tip.css(s).animate({left:this.pos.l,top:this.pos.t},200,function(){p.asyncAnimating=!1})}else this.$tip.css({left:this.pos.l,top:this.pos.t})}}}(window.jQuery),function(e){"use strict";var t=function(e){this.init("dateui",e,t.defaults),this.initPicker(e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{initPicker:function(t,n){this.options.viewformat||(this.options.viewformat=this.options.format),this.options.viewformat=this.options.viewformat.replace("yyyy","yy"),this.options.format=this.options.format.replace("yyyy","yy"),this.options.datepicker=e.extend({},n.datepicker,t.datepicker,{dateFormat:this.options.viewformat})},render:function(){this.$input.datepicker(this.options.datepicker),this.options.clear&&(this.$clear=e('<a href="#"></a>').html(this.options.clear).click(e.proxy(function(e){e.preventDefault(),e.stopPropagation(),this.clear()},this)),this.$tpl.parent().append(e('<div class="editable-clear">').append(this.$clear)))},value2html:function(n,r){var i=e.datepicker.formatDate(this.options.viewformat,n);t.superclass.value2html(i,r)},html2value:function(t){if(typeof t!="string")return t;var n;try{n=e.datepicker.parseDate(this.options.viewformat,t)}catch(r){}return n},value2str:function(t){return e.datepicker.formatDate(this.options.format,t)},str2value:function(t){if(typeof t!="string")return t;var n;try{n=e.datepicker.parseDate(this.options.format,t)}catch(r){}return n},value2submit:function(e){return this.value2str(e)},value2input:function(e){this.$input.datepicker("setDate",e)},input2value:function(){return this.$input.datepicker("getDate")},activate:function(){},clear:function(){this.$input.datepicker("setDate",null)},autosubmit:function(){this.$input.on("mouseup","table.ui-datepicker-calendar a.ui-state-default",function(t){var n=e(this).closest("form");setTimeout(function(){n.submit()},200)})}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<div class="editable-date"></div>',inputclass:null,format:"yyyy-mm-dd",viewformat:null,datepicker:{firstDay:0,changeYear:!0,changeMonth:!0,showOtherMonths:!0},clear:"× clear"}),e.fn.editabletypes.dateui=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("dateuifield",e,t.defaults),this.initPicker(e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.dateui),e.extend(t.prototype,{render:function(){this.$input.datepicker(this.options.datepicker),e.fn.editabletypes.text.prototype.renderClear.call(this)},value2input:function(t){this.$input.val(e.datepicker.formatDate(this.options.viewformat,t))},input2value:function(){return this.html2value(this.$input.val())},activate:function(){e.fn.editabletypes.text.prototype.activate.call(this)},toggleClear:function(){e.fn.editabletypes.text.prototype.toggleClear.call(this)},autosubmit:function(){}}),t.defaults=e.extend({},e.fn.editabletypes.dateui.defaults,{tpl:'<input type="text"/>',inputclass:null,datepicker:{showOn:"button",buttonImage:"http://jqueryui.com/resources/demos/datepicker/images/calendar.gif",buttonImageOnly:!0,firstDay:0,changeYear:!0,changeMonth:!0,showOtherMonths:!0},clear:!1}),e.fn.editabletypes.dateuifield=t}(window.jQuery); \ No newline at end of file diff --git a/dist/jqueryui-editable/css/jqueryui-editable.css b/dist/jqueryui-editable/css/jqueryui-editable.css new file mode 100644 index 0000000..b0863f8 --- /dev/null +++ b/dist/jqueryui-editable/css/jqueryui-editable.css @@ -0,0 +1,193 @@ +/*! X-editable - v1.4.4 +* 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 */ +} + +.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-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 */ +} +.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: '*'*/ +} + +/*see https://github.com/vitalets/x-editable/issues/139 */ +.form-horizontal .editable +{ + padding-top: 5px; + display:inline-block; +} + diff --git a/dist/jqueryui-editable/img/clear.png b/dist/jqueryui-editable/img/clear.png new file mode 100644 index 0000000..580b52a Binary files /dev/null and b/dist/jqueryui-editable/img/clear.png differ diff --git a/dist/jqueryui-editable/img/loading.gif b/dist/jqueryui-editable/img/loading.gif new file mode 100644 index 0000000..5b33f7e Binary files /dev/null and b/dist/jqueryui-editable/img/loading.gif differ diff --git a/dist/jqueryui-editable/js/jqueryui-editable.js b/dist/jqueryui-editable/js/jqueryui-editable.js new file mode 100644 index 0000000..fc7ac02 --- /dev/null +++ b/dist/jqueryui-editable/js/jqueryui-editable.js @@ -0,0 +1,4578 @@ +/*! X-editable - v1.4.4 +* 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); + }, + 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(); + + /** + Fired when rendering starts + @event rendering + @param {Object} event event object + **/ + this.$div.triggerHandler('rendering'); + + //init input + this.initInput(); + + //append input to form + this.input.prerender(); + 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'); + this.input.value2input(this.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 <br> for more pretty error display + if(msg) { + lines = msg.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('<div>').text(lines[i]).html(); + } + msg = lines.join('<br>'); + } + $group.addClass($.fn.editableform.errorGroupClass); + $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); + } + }, + + submit: function(e) { + e.stopPropagation(); + e.preventDefault(); + + var error, + newValue = this.input.input2value(); //get new value from input + + //validation + if (error = this.validate(newValue)) { + 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; + } + + //sending data to server + $.when(this.save(newValue)) + .done($.proxy(function(response) { + //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: <something>} --> 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 submitted value + @param {Object} params.response ajax response + + @example + $('#form-div').on('save'), function(e, params){ + if(params.newValue === 'username') {...} + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, response: response}); + }, this)) + .fail($.proxy(function(xhr) { + 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(newValue) { + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + //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 = !!(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 <code>text|textarea|select|date|checklist</code> + + @property type + @type string + @default 'text' + **/ + type: 'text', + /** + Url for submit, e.g. <code>'/post'</code> + 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 <code>object</code> - it is **appended** to original ajax data (pk, name and value). + If defined as <code>function</code> - 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 <code>id</code> 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. <code>{id: 1, lang: 'en'}</code>. + 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, + /** + Strategy for sending data on server. Can be <code>auto|always|never</code>. + When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element. + + @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. + + @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 <code>{success: true}</code> + or <code>{success: false, msg: "server error"}</code> 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** <code>{newValue: <something>}</code> - 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 <code>null</code> 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 = '<form class="form-inline editableform">'+ + '<div class="control-group">' + + '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+ + '<div class="editable-error-block"></div>' + + '</div>' + + '</form>'; + + //loading div + $.fn.editableform.loading = '<div class="editableform-loading"></div>'; + + //buttons + $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+ + '<button type="button" class="editable-cancel">cancel</button>'; + + //error class attached to control-group + $.fn.editableform.errorGroupClass = null; + + //error class attached to editable-error-block + $.fn.editableform.errorBlockClass = 'editable-error'; +}(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: <a data-source="{'a': 'b', 'c': 'd'}"> + * 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 $('<div>').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 []; + } + + valueProp = valueProp || 'value'; + + 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' ? o[valueProp] : o); }).length) { + result.push(o); + } + } else { + if(value == (o && typeof o === 'object' ? o[valueProp] : o)) { + 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; + } + } + + }; +}(window.jQuery)); + +/** +Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br> +This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br> +Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br> +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, //tbd in child class + innerCss: null, //tbd in child class + containerClass: 'editable-container editable-popup', //css class applied to container element + 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(); + + //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<exclude_classes.length; i++) { + if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) { + return; + } + } + + //close all open containers (except one - target) + Popup.prototype.closeOthers(e.target); + }); + + $(document).data('editable-handlers-attached', true); + } + }, + + //split options on containerOptions and formOptions + splitOptions: function() { + this.containerOptions = {}; + this.formOptions = {}; + + if(!$.fn[this.containerName]) { + throw new Error(this.containerName + ' not found. Have you included corresponding js file?'); + } + + var cDef = $.fn[this.containerName].defaults; + //keys defined in container defaults go to container, others go to form + for(var k in this.options) { + if(k in cDef) { + this.containerOptions[k] = this.options[k]; + } else { + this.formOptions[k] = this.options[k]; + } + } + }, + + /* + Returns jquery object of container + @method tip() + */ + tip: function() { + return this.container() ? this.container().$tip : null; + }, + + /* returns container object */ + container: function() { + return this.$element.data(this.containerDataName || this.containerName); + }, + + /* call native method of underlying container, e.g. this.$element.popover('method') */ + call: function() { + this.$element[this.containerName].apply(this.$element, arguments); + }, + + initContainer: function(){ + this.call(this.containerOptions); + }, + + renderForm: function() { + this.$form + .editableform(this.formOptions) + .on({ + save: $.proxy(this.save, this), //click on submit button (value changed) + nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed) + cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button + show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state) + rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown + resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed + rendered: $.proxy(function(){ + /** + Fired when container is shown and form is rendered (for select will wait for loading dropdown options). + **Note:** Bootstrap popover has own `shown` 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 shown + @param {Object} event event object + @example + $('#username').on('shown', function(e, editable) { + editable.input.$input.val('overwriting value of input..'); + }); + **/ + /* + TODO: added second param mainly to distinguish from bootstrap's shown event. It's a hotfix that will be solved in future versions via namespaced events. + */ + this.$element.triggerHandler('shown', this); + }, this) + }) + .editableform('render'); + }, + + /** + Shows container with form + @method show() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + /* Note: poshytip owerwrites this method totally! */ + show: function (closeAll) { + this.$element.addClass('editable-open'); + if(closeAll !== false) { + //close all open containers (except this) + this.closeOthers(this.$element[0]); + } + + //show container itself + this.innerShow(); + this.tip().addClass(this.containerClass); + + /* + Currently, form is re-rendered on every show. + The main reason is that we dont know, what container will do with content when closed: + remove(), detach() or just hide(). + + Detaching form itself before hide and re-insert before show is good solution, + but visually it looks ugly, as container changes size before hide. + */ + + //if form already exist - delete previous data + if(this.$form) { + //todo: destroy prev data! + //this.$form.destroy(); + } + + this.$form = $('<div>'); + + //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 <code>save|cancel|onblur|nochange|undefined (=manual)</code> + **/ + hide: function(reason) { + if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { + return; + } + + 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 <code>save|cancel|onblur|nochange|manual</code> + @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 <code>$(this).data('editableContainer')</code> 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 od 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 <code>top|right|bottom|left</code>. 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 <code>cancel|submit|ignore</code>. + Setting <code>ignore</code> allows to have several containers open. + + @property onblur + @type string + @default 'cancel' + @since 1.1.1 + **/ + onblur: 'cancel', + + /** + Animation speed (inline mode) + @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 <span> element + this.$tip = $('<span></span>'); + + //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(); + } + }; + + 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'); + + //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 + 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 + } + + //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; + } + + this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === ''; + + //emptytext shown only for enabled + if(!this.options.disabled) { + if (this.isEmpty) { + this.$element.text(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); + } + } + + //set new value + this.setValue(params.newValue, false, params.response); + + /** + Fired when new value was submitted. You can use <code>$(this).data('editable')</code> 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(); + } + + 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. + + @method getValue() + @returns {Object} object of element names and values + @example + $('#username, #fullname').editable('getValue'); + // possible result: + { + username: "superuser", + fullname: "John" + } + **/ + case 'getValue': + 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 <a href="#newrecord">creating new records</a> for details. + + @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'), + values; + + if($.isEmptyObject(errors)) { + values = this.editable('getValue'); + if(config.data) { + $.extend(values, config.data); + } + + $.ajax($.extend({ + url: config.url, + data: values, + type: 'POST' + }, config.ajaxOptions)) + .success(function(response) { + //successful response 200 OK + if(typeof config.success === 'function') { + config.success.call($elems, response, config); + } + }) + .error(function(){ //ajax error + if(typeof config.error === 'function') { + config.error.apply($elems, arguments); + } + }); + } 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; + + 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 <code>text|textarea|select|date|checklist</code> 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 <code>click|dblclick|mouseenter|manual</code>. + When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable. + **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element, + you need to apply <code>e.stopPropagation()</code> 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 <code>auto|always|never</code>. Useful for select and date. + For example, if dropdown list is <code>{1: 'a', 2: 'b'}</code> and element's value set to <code>1</code>, it's html will be automatically set to <code>'a'</code>. + <code>auto</code> - text will be automatically set only if element is empty. + <code>always|never</code> - 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 + <a id="price" data-type="text" data-value="100"></a> + <script> + $('#price').editable({ + ... + display: function(value) { + $(this).text(value + '$'); + } + }) + </script> + + @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 + <div id="user"> + <!-- empty --> + <a href="#" data-name="username" data-type="text" class="editable-click editable-empty" data-value="" title="Username">Empty</a> + <!-- non-empty --> + <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" class="editable-click" title="Group">Operator</a> + </div> + + <script> + $('#user').editable({ + selector: 'a', + url: '/post', + pk: 1 + }); + </script> + **/ + selector: null + }; + +}(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).text(value); + }, + + /** + Converts element's html to value + + @method html2value(html) + @param {string} html + @returns {mixed} + **/ + html2value: function(html) { + return $('<div>').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 $('<div>').text(str).html(); + }, + + /** + attach handler to automatically submit form when value changed (useful when buttons not shown) + **/ + autosubmit: 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 input-medium + **/ + inputclass: 'input-medium', + //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) { + //if allready loaded just call success + if($.isArray(this.sourceData)) { + success.call(this); + return; + } + + // try parse json in single quotes (for double quotes jquery does automatically) + try { + this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false); + } catch (e) { + error.call(this); + return; + } + + var source = this.options.source; + + //run source if it function + if ($.isFunction(source)) { + source = source.call(this.options.scope); + } + + //loading from url + if (typeof source === 'string') { + //try to get 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 = []; + } + } + + //loading sourceData from server + $.ajax({ + 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) + }); + } 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 <code>true</code> 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 + }); + + $.fn.editabletypes.list = List; + +}(window.jQuery)); + +/** +Text input + +@class text +@extends abstractinput +@final +@example +<a href="#" id="username" data-type="text" data-pk="1">awesome</a> +<script> +$(function(){ + $('#username').editable({ + url: '/post', + title: 'Enter username' + }); +}); +</script> +**/ +(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 = $('<span class="editable-clear-x"></span>'); + 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 <input type="text"> + **/ + tpl: '<input type="text">', + /** + 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 +<a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a> +<script> +$(function(){ + $('#comments').editable({ + url: '/post', + title: 'Enter comments', + rows: 10 + }); +}); +</script> +**/ +(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(); + } + }); + }, + + value2html: function(value, element) { + var html = '', lines; + if(value) { + lines = value.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('<div>').text(lines[i]).html(); + } + html = lines.join('<br>'); + } + $(element).html(html); + }, + + html2value: function(html) { + if(!html) { + return ''; + } + + var regex = new RegExp(String.fromCharCode(10), 'g'); + var lines = html.split(/<br\s*\/?>/i); + for (var i = 0; i < lines.length; i++) { + var text = $('<div>').html(lines[i]).text(); + + // Remove newline characters (\n) to avoid them being converted by value2html() method + // thus adding extra <br> 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 <textarea></textarea> + **/ + tpl:'<textarea></textarea>', + /** + @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 +<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a> +<script> +$(function(){ + $('#status').editable({ + value: 2, + source: [ + {value: 1, text: 'Active'}, + {value: 2, text: 'Blocked'}, + {value: 3, text: 'Deleted'} + ] + }); +}); +</script> +**/ +(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) { + if($.isArray(data)) { + for(var i=0; i<data.length; i++) { + if(data[i].children) { + $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children)); + } else { + $el.append($('<option>', {value: data[i].value}).text(data[i].text)); + } + } + } + return $el; + }; + + fillItems(this.$input, this.sourceData); + + this.setClass(); + + //enter submit + this.$input.on('keydown.editable', function (e) { + if (e.which === 13) { + $(this).closest('form').submit(); + } + }); + }, + + value2htmlFinal: function(value, element) { + var text = '', + items = $.fn.editableutils.itemsByValue(value, this.sourceData); + + if(items.length) { + text = items[0].text; + } + + $(element).text(text); + }, + + autosubmit: function() { + this.$input.off('keydown.editable').on('change.editable', function(){ + $(this).closest('form').submit(); + }); + } + }); + + Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { + /** + @property tpl + @default <select></select> + **/ + tpl:'<select></select>' + }); + + $.fn.editabletypes.select = Select; + +}(window.jQuery)); + +/** +List of checkboxes. +Internally value stored as javascript array of values. + +@class checklist +@extends list +@final +@example +<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-original-title="Select options"></a> +<script> +$(function(){ + $('#options').editable({ + value: [2, 3], + source: [ + {value: 1, text: 'option1'}, + {value: 2, text: 'option2'}, + {value: 3, text: 'option3'} + ] + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Checklist = function (options) { + this.init('checklist', options, Checklist.defaults); + }; + + $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list); + + $.extend(Checklist.prototype, { + renderList: function() { + var $label, $div; + + this.$tpl.empty(); + + if(!$.isArray(this.sourceData)) { + return; + } + + for(var i=0; i<this.sourceData.length; i++) { + $label = $('<label>').append($('<input>', { + type: 'checkbox', + value: this.sourceData[i].value + })) + .append($('<span>').text(' '+this.sourceData[i].text)); + + $('<div>').append($label).appendTo(this.$tpl); + } + + this.$input = this.$tpl.find('input[type="checkbox"]'); + this.setClass(); + }, + + value2str: function(value) { + return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : ''; + }, + + //parse separated string + str2value: function(str) { + var reg, value = null; + if(typeof str === 'string' && str.length) { + reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*'); + value = str.split(reg); + } else if($.isArray(str)) { + value = str; + } else { + value = [str]; + } + return value; + }, + + //set checked on required checkboxes + value2input: function(value) { + this.$input.prop('checked', false); + if($.isArray(value) && value.length) { + this.$input.each(function(i, el) { + var $el = $(el); + // cannot use $.inArray as it performs strict comparison + $.each(value, function(j, val){ + /*jslint eqeq: true*/ + if($el.val() == val) { + /*jslint eqeq: false*/ + $el.prop('checked', true); + } + }); + }); + } + }, + + input2value: function() { + var checked = []; + this.$input.filter(':checked').each(function(i, el) { + checked.push($(el).val()); + }); + return checked; + }, + + //collect text of checked boxes + value2htmlFinal: function(value, element) { + var html = [], + checked = $.fn.editableutils.itemsByValue(value, this.sourceData); + + if(checked.length) { + $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); + $(element).html(html.join('<br>')); + } else { + $(element).empty(); + } + }, + + activate: function() { + this.$input.first().focus(); + }, + + autosubmit: function() { + this.$input.on('keydown', function(e){ + if (e.which === 13) { + $(this).closest('form').submit(); + } + }); + } + }); + + Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, { + /** + @property tpl + @default <div></div> + **/ + tpl:'<div class="editable-checklist"></div>', + + /** + @property inputclass + @type string + @default null + **/ + inputclass: null, + + /** + Separator of values when reading from `data-value` attribute + + @property separator + @type string + @default ',' + **/ + separator: ',' + }); + + $.fn.editabletypes.checklist = Checklist; + +}(window.jQuery)); + +/** +HTML5 input types. +Following types are supported: + +* password +* email +* url +* tel +* number +* range + +Learn more about html5 inputs: +http://www.w3.org/wiki/HTML5_form_additions +To check browser compatibility please see: +https://developer.mozilla.org/en-US/docs/HTML/Element/Input + +@class html5types +@extends text +@final +@since 1.3.0 +@example +<a href="#" id="email" data-type="email" data-pk="1">admin@example.com</a> +<script> +$(function(){ + $('#email').editable({ + url: '/post', + title: 'Enter email' + }); +}); +</script> +**/ + +/** +@property tpl +@default depends on type +**/ + +/* +Password +*/ +(function ($) { + "use strict"; + + var Password = function (options) { + this.init('password', options, Password.defaults); + }; + $.fn.editableutils.inherit(Password, $.fn.editabletypes.text); + $.extend(Password.prototype, { + //do not display password, show '[hidden]' instead + value2html: function(value, element) { + if(value) { + $(element).text('[hidden]'); + } else { + $(element).empty(); + } + }, + //as password not displayed, should not set value by html + html2value: function(html) { + return null; + } + }); + Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="password">' + }); + $.fn.editabletypes.password = Password; +}(window.jQuery)); + + +/* +Email +*/ +(function ($) { + "use strict"; + + var Email = function (options) { + this.init('email', options, Email.defaults); + }; + $.fn.editableutils.inherit(Email, $.fn.editabletypes.text); + Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="email">' + }); + $.fn.editabletypes.email = Email; +}(window.jQuery)); + + +/* +Url +*/ +(function ($) { + "use strict"; + + var Url = function (options) { + this.init('url', options, Url.defaults); + }; + $.fn.editableutils.inherit(Url, $.fn.editabletypes.text); + Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="url">' + }); + $.fn.editabletypes.url = Url; +}(window.jQuery)); + + +/* +Tel +*/ +(function ($) { + "use strict"; + + var Tel = function (options) { + this.init('tel', options, Tel.defaults); + }; + $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text); + Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="tel">' + }); + $.fn.editabletypes.tel = Tel; +}(window.jQuery)); + + +/* +Number +*/ +(function ($) { + "use strict"; + + var NumberInput = function (options) { + this.init('number', options, NumberInput.defaults); + }; + $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text); + $.extend(NumberInput.prototype, { + render: function () { + NumberInput.superclass.render.call(this); + this.setAttr('min'); + this.setAttr('max'); + this.setAttr('step'); + }, + postrender: function() { + if(this.$clear) { + //increase right ffset for up/down arrows + this.$clear.css({right: 24}); + /* + //can position clear button only here, when form is shown and height can be calculated + var h = this.$input.outerHeight(true) || 20, + delta = (h - this.$clear.height()) / 2; + + //add 12px to offset right for up/down arrows + this.$clear.css({top: delta, right: delta + 16}); + */ + } + } + }); + NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, { + tpl: '<input type="number">', + inputclass: 'input-mini', + min: null, + max: null, + step: null + }); + $.fn.editabletypes.number = NumberInput; +}(window.jQuery)); + + +/* +Range (inherit from number) +*/ +(function ($) { + "use strict"; + + var Range = function (options) { + this.init('range', options, Range.defaults); + }; + $.fn.editableutils.inherit(Range, $.fn.editabletypes.number); + $.extend(Range.prototype, { + render: function () { + this.$input = this.$tpl.filter('input'); + + this.setClass(); + this.setAttr('min'); + this.setAttr('max'); + this.setAttr('step'); + + this.$input.on('input', function(){ + $(this).siblings('output').text($(this).val()); + }); + }, + activate: function() { + this.$input.focus(); + } + }); + Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, { + tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>', + inputclass: 'input-medium' + }); + $.fn.editabletypes.range = Range; +}(window.jQuery)); +/** +Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2. +Please see [original docs](http://ivaynberg.github.com/select2) for detailed description and options. +You should manually include select2 distributive: + + <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> + <script src="select2/select2.js"></script> + +For make it **Bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css): + + <link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link> + +**Note:** currently `ajax` source for select2 is not supported, as it's not possible to load it in closed select2 state. +The solution is to load source manually and assign statically. + +@class select2 +@extends abstractinput +@since 1.4.1 +@final +@example +<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-original-title="Select country"></a> +<script> +$(function(){ + $('#country').editable({ + source: [ + {id: 'gb', text: 'Great Britain'}, + {id: 'us', text: 'United States'}, + {id: 'ru', text: 'Russia'} + ], + select2: { + multiple: true + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var Constructor = function (options) { + this.init('select2', options, Constructor.defaults); + + options.select2 = options.select2 || {}; + + var that = this, + mixin = { //mixin to select2 options + placeholder: options.placeholder + }; + + //detect whether it is multi-valued + this.isMultiple = options.select2.tags || options.select2.multiple; + + //if not `tags` mode, we need define initSelection to set data from source + if(!options.select2.tags) { + if(options.source) { + mixin.data = options.source; + } + + //this function can be defaulted in seletc2. See https://github.com/ivaynberg/select2/issues/710 + mixin.initSelection = function (element, callback) { + //temp: try update results + /* + if(options.select2 && options.select2.ajax) { + console.log('attached'); + var original = $(element).data('select2').postprocessResults; + console.log(original); + $(element).data('select2').postprocessResults = function(data, initial) { + console.log('postprocess'); + // this.element.triggerHandler('loaded', [data]); + original.apply(this, arguments); + } + + // $(element).on('loaded', function(){console.log('loaded');}); + $(element).data('select2').updateResults(true); + } + */ + + var val = that.str2value(element.val()), + data = $.fn.editableutils.itemsByValue(val, mixin.data, 'id'); + + //for single-valued mode should not use array. Take first element instead. + if($.isArray(data) && data.length && !that.isMultiple) { + data = data[0]; + } + + callback(data); + }; + } + + //overriding objects in config (as by default jQuery extend() is not recursive) + this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2); + }; + + $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); + + $.extend(Constructor.prototype, { + render: function() { + this.setClass(); + //apply select2 + this.$input.select2(this.options.select2); + + //when data is loaded via ajax, we need to know when it's done + if('ajax' in this.options.select2) { + /* + console.log('attached'); + var original = this.$input.data('select2').postprocessResults; + this.$input.data('select2').postprocessResults = function(data, initial) { + this.element.triggerHandler('loaded', [data]); + original.apply(this, arguments); + } + */ + } + + + //trigger resize of editableform to re-position container in multi-valued mode + if(this.isMultiple) { + this.$input.on('change', function() { + $(this).closest('form').parent().triggerHandler('resize'); + }); + } + }, + + value2html: function(value, element) { + var text = '', data; + if(this.$input) { //called when submitting form and select2 already exists + data = this.$input.select2('data'); + } else { //on init (autotext) + //here select2 instance not created yet and data may be even not loaded. + //we can check data/tags property of select config and if exist lookup text + if(this.options.select2.tags) { + data = value; + } else if(this.options.select2.data) { + data = $.fn.editableutils.itemsByValue(value, this.options.select2.data, 'id'); + } else { + //if('ajax' in this.options.select2) { + } + } + + if($.isArray(data)) { + //collect selected data and show with separator + text = []; + $.each(data, function(k, v){ + text.push(v && typeof v === 'object' ? v.text : v); + }); + } else if(data) { + text = data.text; + } + + text = $.isArray(text) ? text.join(this.options.viewseparator) : text; + + $(element).text(text); + }, + + html2value: function(html) { + return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null; + }, + + value2input: function(value) { + this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + }, + + input2value: function() { + return this.$input.select2('val'); + }, + + str2value: function(str, separator) { + if(typeof str !== 'string' || !this.isMultiple) { + return str; + } + + separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator; + + var val, i, l; + + if (str === null || str.length < 1) { + return null; + } + val = str.split(separator); + for (i = 0, l = val.length; i < l; i = i + 1) { + val[i] = $.trim(val[i]); + } + + return val; + }, + + autosubmit: function() { + this.$input.on('change', function(e, isInitial){ + if(!isInitial) { + $(this).closest('form').submit(); + } + }); + } + + }); + + Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <input type="hidden"> + **/ + tpl:'<input type="hidden">', + /** + Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2). + + @property select2 + @type object + @default null + **/ + select2: null, + /** + Placeholder attribute of select + + @property placeholder + @type string + @default null + **/ + placeholder: null, + /** + Source data for select. It will be assigned to select2 `data` property and kept here just for convenience. + Please note, that format is different from simple `select` input: use 'id' instead of 'value'. + E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`. + + @property source + @type array + @default null + **/ + source: null, + /** + Separator used to display tags. + + @property viewseparator + @type string + @default ', ' + **/ + viewseparator: ', ' + }); + + $.fn.editabletypes.select2 = Constructor; + +}(window.jQuery)); + +/** +* Combodate - 1.0.3 +* Dropdown date and time picker. +* Converts text input into dropdowns to pick day, month, year, hour, minute and second. +* Uses momentjs as datetime library http://momentjs.com. +* For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang +* +* Author: Vitaliy Potapov +* Project page: http://github.com/vitalets/combodate +* Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. +**/ +(function ($) { + + var Combodate = function (element, options) { + this.$element = $(element); + if(!this.$element.is('input')) { + $.error('Combodate should be applied to INPUT element'); + return; + } + this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data()); + this.init(); + }; + + Combodate.prototype = { + constructor: Combodate, + init: function () { + this.map = { + //key regexp moment.method + day: ['D', 'date'], + month: ['M', 'month'], + year: ['Y', 'year'], + hour: ['[Hh]', 'hours'], + minute: ['m', 'minutes'], + second: ['s', 'seconds'], + ampm: ['[Aa]', ''] + }; + + this.$widget = $('<span class="combodate"></span>').html(this.getTemplate()); + + this.initCombos(); + + //update original input on change + this.$widget.on('change', 'select', $.proxy(function(){ + this.$element.val(this.getValue()); + }, this)); + + this.$widget.find('select').css('width', 'auto'); + + //hide original input and insert widget + this.$element.hide().after(this.$widget); + + //set initial value + this.setValue(this.$element.val() || this.options.value); + }, + + /* + Replace tokens in template with <select> elements + */ + getTemplate: function() { + var tpl = this.options.template; + + //first pass + $.each(this.map, function(k, v) { + v = v[0]; + var r = new RegExp(v+'+'), + token = v.length > 1 ? v.substring(1, 2) : v; + + tpl = tpl.replace(r, '{'+token+'}'); + }); + + //replace spaces with + tpl = tpl.replace(/ /g, ' '); + + //second pass + $.each(this.map, function(k, v) { + v = v[0]; + var token = v.length > 1 ? v.substring(1, 2) : v; + + tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>'); + }); + + return tpl; + }, + + /* + Initialize combos that presents in template + */ + initCombos: function() { + var that = this; + $.each(this.map, function(k, v) { + var $c = that.$widget.find('.'+k), f, items; + if($c.length) { + that['$'+k] = $c; //set properties like this.$day, this.$month etc. + f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); //define method name to fill items, e.g `fillDays` + items = that[f](); + that['$'+k].html(that.renderItems(items)); + } + }); + }, + + /* + Initialize items of combos. Handles `firstItem` option + */ + initItems: function(key) { + var values = [], + relTime; + + if(this.options.firstItem === 'name') { + //need both to support moment ver < 2 and >= 2 + relTime = moment.relativeTime || moment.langData()._relativeTime; + var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key]; + //take last entry (see momentjs lang files structure) + header = header.split(' ').reverse()[0]; + values.push(['', header]); + } else if(this.options.firstItem === 'empty') { + values.push(['', '']); + } + return values; + }, + + /* + render items to string of <option> tags + */ + renderItems: function(items) { + var str = []; + for(var i=0; i<items.length; i++) { + str.push('<option value="'+items[i][0]+'">'+items[i][1]+'</option>'); + } + return str.join("\n"); + }, + + /* + fill day + */ + fillDay: function() { + var items = this.initItems('d'), name, i, + twoDigit = this.options.template.indexOf('DD') !== -1; + + for(i=1; i<=31; i++) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill month + */ + fillMonth: function() { + var items = this.initItems('M'), name, i, + longNames = this.options.template.indexOf('MMMM') !== -1, + shortNames = this.options.template.indexOf('MMM') !== -1, + twoDigit = this.options.template.indexOf('MM') !== -1; + + for(i=0; i<=11; i++) { + if(longNames) { + name = moment().month(i).format('MMMM'); + } else if(shortNames) { + name = moment().month(i).format('MMM'); + } else if(twoDigit) { + name = this.leadZero(i+1); + } else { + name = i+1; + } + items.push([i, name]); + } + return items; + }, + + /* + fill year + */ + fillYear: function() { + var items = [], name, i, + longNames = this.options.template.indexOf('YYYY') !== -1; + + for(i=this.options.maxYear; i>=this.options.minYear; i--) { + name = longNames ? i : (i+'').substring(2); + items[this.options.yearDescending ? 'push' : 'unshift']([i, name]); + } + + items = this.initItems('y').concat(items); + + return items; + }, + + /* + fill hour + */ + fillHour: function() { + var items = this.initItems('h'), name, i, + h12 = this.options.template.indexOf('h') !== -1, + h24 = this.options.template.indexOf('H') !== -1, + twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1, + max = h12 ? 12 : 23; + + for(i=0; i<=max; i++) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill minute + */ + fillMinute: function() { + var items = this.initItems('m'), name, i, + twoDigit = this.options.template.indexOf('mm') !== -1; + + for(i=0; i<=59; i+= this.options.minuteStep) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill second + */ + fillSecond: function() { + var items = this.initItems('s'), name, i, + twoDigit = this.options.template.indexOf('ss') !== -1; + + for(i=0; i<=59; i+= this.options.secondStep) { + name = twoDigit ? this.leadZero(i) : i; + items.push([i, name]); + } + return items; + }, + + /* + fill ampm + */ + fillAmpm: function() { + var ampmL = this.options.template.indexOf('a') !== -1, + ampmU = this.options.template.indexOf('A') !== -1, + items = [ + ['am', ampmL ? 'am' : 'AM'], + ['pm', ampmL ? 'pm' : 'PM'] + ]; + return items; + }, + + /* + Returns current date value. + If format not specified - `options.format` used. + If format = `null` - Moment object returned. + */ + getValue: function(format) { + var dt, values = {}, + that = this, + notSelected = false; + + //getting selected values + $.each(this.map, function(k, v) { + if(k === 'ampm') { + return; + } + var def = k === 'day' ? 1 : 0; + + values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def; + + if(isNaN(values[k])) { + notSelected = true; + return false; + } + }); + + //if at least one visible combo not selected - return empty string + if(notSelected) { + return ''; + } + + //convert hours if 12h format + if(this.$ampm) { + values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; + if(values.hour === 24) { + values.hour = 0; + } + } + + dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]); + + //highlight invalid date + this.highlight(dt); + + format = format === undefined ? this.options.format : format; + if(format === null) { + return dt.isValid() ? dt : null; + } else { + return dt.isValid() ? dt.format(format) : ''; + } + }, + + setValue: function(value) { + if(!value) { + return; + } + + var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value), + that = this, + values = {}; + + //function to find nearest value in select options + function getNearest($select, value) { + var delta = {}; + $select.children('option').each(function(i, opt){ + var optValue = $(opt).attr('value'), + distance; + + if(optValue === '') return; + distance = Math.abs(optValue - value); + if(typeof delta.distance === 'undefined' || distance < delta.distance) { + delta = {value: optValue, distance: distance}; + } + }); + return delta.value; + } + + if(dt.isValid()) { + //read values from date object + $.each(this.map, function(k, v) { + if(k === 'ampm') { + return; + } + values[k] = dt[v[1]](); + }); + + if(this.$ampm) { + if(values.hour > 12) { + values.hour -= 12; + values.ampm = 'pm'; + } else { + values.ampm = 'am'; + } + } + + $.each(values, function(k, v) { + //call val() for each existing combo, e.g. this.$hour.val() + if(that['$'+k]) { + + if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } + + if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) { + v = getNearest(that['$'+k], v); + } + + that['$'+k].val(v); + } + }); + + this.$element.val(dt.format(this.options.format)); + } + }, + + /* + highlight combos if date is invalid + */ + highlight: function(dt) { + if(!dt.isValid()) { + if(this.options.errorClass) { + this.$widget.addClass(this.options.errorClass); + } else { + //store original border color + if(!this.borderColor) { + this.borderColor = this.$widget.find('select').css('border-color'); + } + this.$widget.find('select').css('border-color', 'red'); + } + } else { + if(this.options.errorClass) { + this.$widget.removeClass(this.options.errorClass); + } else { + this.$widget.find('select').css('border-color', this.borderColor); + } + } + }, + + leadZero: function(v) { + return v <= 9 ? '0' + v : v; + }, + + destroy: function() { + this.$widget.remove(); + this.$element.removeData('combodate').show(); + } + + //todo: clear method + }; + + $.fn.combodate = function ( option ) { + var d, args = Array.apply(null, arguments); + args.shift(); + + //getValue returns date as string / object (not jQuery object) + if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) { + return d.getValue.apply(d, args); + } + + return this.each(function () { + var $this = $(this), + data = $this.data('combodate'), + options = typeof option == 'object' && option; + if (!data) { + $this.data('combodate', (data = new Combodate(this, options))); + } + if (typeof option == 'string' && typeof data[option] == 'function') { + data[option].apply(data, args); + } + }); + }; + + $.fn.combodate.defaults = { + //in this format value stored in original input + format: 'DD-MM-YYYY HH:mm', + //in this format items in dropdowns are displayed + template: 'D / MMM / YYYY H : mm', + //initial value, can be `new Date()` + value: null, + minYear: 1970, + maxYear: 2015, + yearDescending: true, + minuteStep: 5, + secondStep: 1, + firstItem: 'empty', //'name', 'empty', 'none' + errorClass: null, + roundTime: true //whether to round minutes and seconds if step > 1 + }; + +}(window.jQuery)); +/** +Combodate input - dropdown date and time picker. +Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com). + + <script src="js/moment.min.js"></script> + +Allows to input: + +* only date +* only time +* both date and time + +Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker. +Internally value stored as `momentjs` object. + +@class combodate +@extends abstractinput +@final +@since 1.4.0 +@example +<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-original-title="Select date"></a> +<script> +$(function(){ + $('#dob').editable({ + format: 'YYYY-MM-DD', + viewformat: 'DD.MM.YYYY', + template: 'D / MMMM / YYYY', + combodate: { + minYear: 2000, + maxYear: 2015, + minuteStep: 1 + } + } + }); +}); +</script> +**/ + +/*global moment*/ + +(function ($) { + "use strict"; + + var Constructor = function (options) { + this.init('combodate', options, Constructor.defaults); + + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } + + //try parse combodate config defined as json string in data-combodate + options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true); + + //overriding combodate config (as by default jQuery extend() is not recursive) + this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, { + format: this.options.format, + template: this.options.template + }); + }; + + $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); + + $.extend(Constructor.prototype, { + render: function () { + this.$input.combodate(this.options.combodate); + + //"clear" link + /* + if(this.options.clear) { + this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ + e.preventDefault(); + e.stopPropagation(); + this.clear(); + }, this)); + + this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); + } + */ + }, + + value2html: function(value, element) { + var text = value ? value.format(this.options.viewformat) : ''; + $(element).text(text); + }, + + html2value: function(html) { + return html ? moment(html, this.options.viewformat) : null; + }, + + value2str: function(value) { + return value ? value.format(this.options.format) : ''; + }, + + str2value: function(str) { + return str ? moment(str, this.options.format) : null; + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + this.$input.combodate('setValue', value); + }, + + input2value: function() { + return this.$input.combodate('getValue', null); + }, + + activate: function() { + this.$input.siblings('.combodate').find('select').eq(0).focus(); + }, + + /* + clear: function() { + this.$input.data('datepicker').date = null; + this.$input.find('.active').removeClass('active'); + }, + */ + + autosubmit: function() { + + } + + }); + + Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <input type="text"> + **/ + tpl:'<input type="text">', + /** + @property inputclass + @default null + **/ + inputclass: null, + /** + Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br> + See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format) + + @property format + @type string + @default YYYY-MM-DD + **/ + format:'YYYY-MM-DD', + /** + Format used for displaying date. Also applied when converting date from element's text on init. + If not specified equals to `format`. + + @property viewformat + @type string + @default null + **/ + viewformat: null, + /** + Template used for displaying dropdowns. + + @property template + @type string + @default D / MMM / YYYY + **/ + template: 'D / MMM / YYYY', + /** + Configuration of combodate. + Full list of options: http://vitalets.github.com/combodate/#docs + + @property combodate + @type object + @default null + **/ + combodate: null + + /* + (not implemented yet) + Text shown as clear date button. + If <code>false</code> clear button will not be rendered. + + @property clear + @type boolean|string + @default 'x clear' + */ + //clear: '× clear' + }); + + $.fn.editabletypes.combodate = Constructor; + +}(window.jQuery)); + +/* +Editableform based on jQuery UI +*/ +(function ($) { + "use strict"; + + $.extend($.fn.editableform.Constructor.prototype, { + initButtons: function() { + var $btn = this.$form.find('.editable-buttons'); + $btn.append($.fn.editableform.buttons); + if(this.options.showbuttons === 'bottom') { + $btn.addClass('editable-buttons-bottom'); + } + + this.$form.find('.editable-submit').button({ + icons: { primary: "ui-icon-check" }, + text: false + }).removeAttr('title'); + this.$form.find('.editable-cancel').button({ + icons: { primary: "ui-icon-closethick" }, + text: false + }).removeAttr('title'); + } + }); + + //error classes + $.fn.editableform.errorGroupClass = null; + $.fn.editableform.errorBlockClass = 'ui-state-error'; + +}(window.jQuery)); +/** +* Editable jQuery UI Tooltip +* --------------------- +* requires jquery ui 1.9.x +*/ +(function ($) { + "use strict"; + + //extend methods + $.extend($.fn.editableContainer.Popup.prototype, { + containerName: 'tooltip', //jQuery method, aplying the widget + containerDataName: 'uiTooltip', //object name in elements .data() (e.g. uiTooltip for tooltip) + innerCss: '.ui-tooltip-content', + + //split options on containerOptions and formOptions + splitOptions: function() { + this.containerOptions = {}; + this.formOptions = {}; + //defaults for tooltip + var cDef = $.ui[this.containerName].prototype.options; + for(var k in this.options) { + if(k in cDef) { + this.containerOptions[k] = this.options[k]; + } else { + this.formOptions[k] = this.options[k]; + } + } + }, + + initContainer: function(){ + this.handlePlacement(); + $.extend(this.containerOptions, { + items: '*', + content: ' ', + track: false, + open: $.proxy(function() { + //disable events hiding tooltip by default + this.container()._on(this.container().element, { + mouseleave: function(e){ e.stopImmediatePropagation(); }, + focusout: function(e){ e.stopImmediatePropagation(); } + }); + }, this) + }); + + this.call(this.containerOptions); + + //disable standart triggering tooltip event + this.container()._off(this.container().element, 'mouseover focusin'); + }, + + tip: function() { + return this.container() ? this.container()._find(this.container().element) : null; + }, + + innerShow: function() { + this.call('open'); + var label = this.options.title || this.$element.data( "ui-tooltip-title") || this.$element.data( "originalTitle"); + this.tip().find(this.innerCss).empty().append($('<label>').text(label)); + }, + + innerHide: function() { + this.call('close'); + }, + + innerDestroy: function() { + /* tooltip destroys itself on hide */ + }, + + setPosition: function() { + this.tip().position( $.extend({ + of: this.$element + }, this.containerOptions.position ) ); + }, + + handlePlacement: function() { + var pos; + switch(this.options.placement) { + case 'top': + pos = { + my: "center bottom-5", + at: "center top" + }; + break; + case 'right': + pos = { + my: "left+5 center", + at: "right center" + }; + break; + case 'bottom': + pos = { + my: "center top+5", + at: "center bottom" + }; + break; + case 'left': + pos = { + my: "right-5 center", + at: "left center" + }; + break; + } + + this.containerOptions.position = pos; + } + + }); + +}(window.jQuery)); + +/** +jQuery UI Datepicker. +Description and examples: http://jqueryui.com/datepicker. +This input is also accessible as **date** type. Do not use it together with __bootstrap-datepicker__ as both apply <code>$().datepicker()</code> method. +For **i18n** you should include js file from here: https://github.com/jquery/jquery-ui/tree/master/ui/i18n. + +@class dateui +@extends abstractinput +@final +@example +<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a> +<script> +$(function(){ + $('#dob').editable({ + format: 'yyyy-mm-dd', + viewformat: 'dd/mm/yyyy', + datepicker: { + firstDay: 1 + } + } + }); +}); +</script> +**/ +(function ($) { + "use strict"; + + var DateUI = function (options) { + this.init('dateui', options, DateUI.defaults); + this.initPicker(options, DateUI.defaults); + }; + + $.fn.editableutils.inherit(DateUI, $.fn.editabletypes.abstractinput); + + $.extend(DateUI.prototype, { + initPicker: function(options, defaults) { + //by default viewformat equals to format + if(!this.options.viewformat) { + this.options.viewformat = this.options.format; + } + + //correct formats: replace yyyy with yy (for compatibility with bootstrap datepicker) + this.options.viewformat = this.options.viewformat.replace('yyyy', 'yy'); + this.options.format = this.options.format.replace('yyyy', 'yy'); + + //overriding datepicker config (as by default jQuery extend() is not recursive) + //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only + this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, { + dateFormat: this.options.viewformat + }); + }, + + render: function () { + this.$input.datepicker(this.options.datepicker); + + //"clear" link + if(this.options.clear) { + this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ + e.preventDefault(); + e.stopPropagation(); + this.clear(); + }, this)); + + this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear)); + } + }, + + value2html: function(value, element) { + var text = $.datepicker.formatDate(this.options.viewformat, value); + DateUI.superclass.value2html(text, element); + }, + + html2value: function(html) { + if(typeof html !== 'string') { + return html; + } + + //if string does not match format, UI datepicker throws exception + var d; + try { + d = $.datepicker.parseDate(this.options.viewformat, html); + } catch(e) {} + + return d; + }, + + value2str: function(value) { + return $.datepicker.formatDate(this.options.format, value); + }, + + str2value: function(str) { + if(typeof str !== 'string') { + return str; + } + + //if string does not match format, UI datepicker throws exception + var d; + try { + d = $.datepicker.parseDate(this.options.format, str); + } catch(e) {} + + return d; + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + this.$input.datepicker('setDate', value); + }, + + input2value: function() { + return this.$input.datepicker('getDate'); + }, + + activate: function() { + }, + + clear: function() { + this.$input.datepicker('setDate', null); + }, + + autosubmit: function() { + this.$input.on('mouseup', 'table.ui-datepicker-calendar a.ui-state-default', function(e){ + var $form = $(this).closest('form'); + setTimeout(function() { + $form.submit(); + }, 200); + }); + } + + }); + + DateUI.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default <div></div> + **/ + tpl:'<div class="editable-date"></div>', + /** + @property inputclass + @default null + **/ + inputclass: null, + /** + Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br> + Full list of tokens: http://docs.jquery.com/UI/Datepicker/formatDate + + @property format + @type string + @default yyyy-mm-dd + **/ + format:'yyyy-mm-dd', + /** + Format used for displaying date. Also applied when converting date from element's text on init. + If not specified equals to <code>format</code> + + @property viewformat + @type string + @default null + **/ + viewformat: null, + + /** + Configuration of datepicker. + Full list of options: http://api.jqueryui.com/datepicker + + @property datepicker + @type object + @default { + firstDay: 0, + changeYear: true, + changeMonth: true + } + **/ + datepicker: { + firstDay: 0, + changeYear: true, + changeMonth: true, + showOtherMonths: true + }, + /** + Text shown as clear date button. + If <code>false</code> clear button will not be rendered. + + @property clear + @type boolean|string + @default 'x clear' + **/ + clear: '× clear' + }); + + $.fn.editabletypes.dateui = DateUI; + +}(window.jQuery)); + +/** +jQuery UI datefield input - modification for inline mode. +Shows normal <input type="text"> and binds popup datepicker. +Automatically shown in inline mode. + +@class dateuifield +@extends dateui + +@since 1.4.0 +**/ +(function ($) { + "use strict"; + + var DateUIField = function (options) { + this.init('dateuifield', options, DateUIField.defaults); + this.initPicker(options, DateUIField.defaults); + }; + + $.fn.editableutils.inherit(DateUIField, $.fn.editabletypes.dateui); + + $.extend(DateUIField.prototype, { + render: function () { + // this.$input = this.$tpl.find('input'); + this.$input.datepicker(this.options.datepicker); + $.fn.editabletypes.text.prototype.renderClear.call(this); + }, + + value2input: function(value) { + this.$input.val($.datepicker.formatDate(this.options.viewformat, value)); + }, + + input2value: function() { + return this.html2value(this.$input.val()); + }, + + activate: function() { + $.fn.editabletypes.text.prototype.activate.call(this); + }, + + toggleClear: function() { + $.fn.editabletypes.text.prototype.toggleClear.call(this); + }, + + autosubmit: function() { + //reset autosubmit to empty + } + }); + + DateUIField.defaults = $.extend({}, $.fn.editabletypes.dateui.defaults, { + /** + @property tpl + @default <input type="text"> + **/ + tpl: '<input type="text"/>', + /** + @property inputclass + @default null + **/ + inputclass: null, + + /* datepicker config */ + datepicker: { + showOn: "button", + buttonImage: "http://jqueryui.com/resources/demos/datepicker/images/calendar.gif", + buttonImageOnly: true, + firstDay: 0, + changeYear: true, + changeMonth: true, + showOtherMonths: true + }, + + /* disable clear link */ + clear: false + }); + + $.fn.editabletypes.dateuifield = DateUIField; + +}(window.jQuery)); \ No newline at end of file diff --git a/dist/jqueryui-editable/js/jqueryui-editable.min.js b/dist/jqueryui-editable/js/jqueryui-editable.min.js new file mode 100644 index 0000000..02f4dde --- /dev/null +++ b/dist/jqueryui-editable/js/jqueryui-editable.min.js @@ -0,0 +1,5 @@ +/*! X-editable - v1.4.4 +* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery +* http://github.com/vitalets/x-editable +* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ +(function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.editableform.defaults,n),this.$div=e(t),this.options.scope||(this.options.scope=this)};t.prototype={constructor:t,initInput:function(){this.input=this.options.input,this.value=this.input.str2value(this.options.value)},initTemplate:function(){this.$form=e(e.fn.editableform.template)},initButtons:function(){var t=this.$form.find(".editable-buttons");t.append(e.fn.editableform.buttons),this.options.showbuttons==="bottom"&&t.addClass("editable-buttons-bottom")},render:function(){this.$loading=e(e.fn.editableform.loading),this.$div.empty().append(this.$loading),this.initTemplate(),this.options.showbuttons?this.initButtons():this.$form.find(".editable-buttons").remove(),this.showLoading(),this.$div.triggerHandler("rendering"),this.initInput(),this.input.prerender(),this.$form.find("div.editable-input").append(this.input.$tpl),this.$div.append(this.$form),e.when(this.input.render()).then(e.proxy(function(){this.options.showbuttons||this.input.autosubmit(),this.$form.find(".editable-cancel").click(e.proxy(this.cancel,this)),this.input.error?(this.error(this.input.error),this.$form.find(".editable-submit").attr("disabled",!0),this.input.$input.attr("disabled",!0),this.$form.submit(function(e){e.preventDefault()})):(this.error(!1),this.input.$input.removeAttr("disabled"),this.$form.find(".editable-submit").removeAttr("disabled"),this.input.value2input(this.value),this.$form.submit(e.proxy(this.submit,this))),this.$div.triggerHandler("rendered"),this.showForm(),this.input.postrender&&this.input.postrender()},this))},cancel:function(){this.$div.triggerHandler("cancel")},showLoading:function(){var e,t;this.$form?(e=this.$form.outerWidth(),t=this.$form.outerHeight(),e&&this.$loading.width(e),t&&this.$loading.height(t),this.$form.hide()):(e=this.$loading.parent().width(),e&&this.$loading.width(e)),this.$loading.show()},showForm:function(e){this.$loading.hide(),this.$form.show(),e!==!1&&this.input.activate(),this.$div.triggerHandler("show")},error:function(t){var n=this.$form.find(".control-group"),r=this.$form.find(".editable-error-block"),i;if(t===!1)n.removeClass(e.fn.editableform.errorGroupClass),r.removeClass(e.fn.editableform.errorBlockClass).empty().hide();else{if(t){i=t.split("\n");for(var s=0;s<i.length;s++)i[s]=e("<div>").text(i[s]).html();t=i.join("<br>")}n.addClass(e.fn.editableform.errorGroupClass),r.addClass(e.fn.editableform.errorBlockClass).html(t).show()}},submit:function(t){t.stopPropagation(),t.preventDefault();var n,r=this.input.input2value();if(n=this.validate(r)){this.error(n),this.showForm();return}if(!this.options.savenochange&&this.input.value2str(r)==this.input.value2str(this.value)){this.$div.triggerHandler("nochange");return}e.when(this.save(r)).done(e.proxy(function(e){var t=typeof this.options.success=="function"?this.options.success.call(this.options.scope,e,r):null;if(t===!1){this.error(!1),this.showForm(!1);return}if(typeof t=="string"){this.error(t),this.showForm();return}t&&typeof t=="object"&&t.hasOwnProperty("newValue")&&(r=t.newValue),this.error(!1),this.value=r,this.$div.triggerHandler("save",{newValue:r,response:e})},this)).fail(e.proxy(function(e){var t;typeof this.options.error=="function"?t=this.options.error.call(this.options.scope,e,r):t=typeof e=="string"?e:e.responseText||e.statusText||"Unknown error!",this.error(t),this.showForm()},this))},save:function(t){var n=this.input.value2submit(t);this.options.pk=e.fn.editableutils.tryParseJson(this.options.pk,!0);var r=typeof this.options.pk=="function"?this.options.pk.call(this.options.scope):this.options.pk,i=!!(typeof this.options.url=="function"||this.options.url&&(this.options.send==="always"||this.options.send==="auto"&&r!==null&&r!==undefined)),s;if(i)return this.showLoading(),s={name:this.options.name||"",value:n,pk:r},typeof this.options.params=="function"?s=this.options.params.call(this.options.scope,s):(this.options.params=e.fn.editableutils.tryParseJson(this.options.params,!0),e.extend(s,this.options.params)),typeof this.options.url=="function"?this.options.url.call(this.options.scope,s):e.ajax(e.extend({url:this.options.url,data:s,type:"POST"},this.options.ajaxOptions))},validate:function(e){e===undefined&&(e=this.value);if(typeof this.options.validate=="function")return this.options.validate.call(this.options.scope,e)},option:function(e,t){e in this.options&&(this.options[e]=t),e==="value"&&this.setValue(t)},setValue:function(e,t){t?this.value=this.input.str2value(e):this.value=e,this.$form&&this.$form.is(":visible")&&this.input.value2input(this.value)}},e.fn.editableform=function(n){var r=arguments;return this.each(function(){var i=e(this),s=i.data("editableform"),o=typeof n=="object"&&n;s||i.data("editableform",s=new t(this,o)),typeof n=="string"&&s[n].apply(s,Array.prototype.slice.call(r,1))})},e.fn.editableform.Constructor=t,e.fn.editableform.defaults={type:"text",url:null,params:null,name:null,pk:null,value:null,send:"auto",validate:null,success:null,error:null,ajaxOptions:null,showbuttons:!0,scope:null,savenochange:!1},e.fn.editableform.template='<form class="form-inline editableform"><div class="control-group"><div><div class="editable-input"></div><div class="editable-buttons"></div></div><div class="editable-error-block"></div></div></form>',e.fn.editableform.loading='<div class="editableform-loading"></div>',e.fn.editableform.buttons='<button type="submit" class="editable-submit">ok</button><button type="button" class="editable-cancel">cancel</button>',e.fn.editableform.errorGroupClass=null,e.fn.editableform.errorBlockClass="editable-error"})(window.jQuery),function(e){"use strict";e.fn.editableutils={inherit:function(e,t){var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e,e.superclass=t.prototype},setCursorPosition:function(e,t){if(e.setSelectionRange)e.setSelectionRange(t,t);else if(e.createTextRange){var n=e.createTextRange();n.collapse(!0),n.moveEnd("character",t),n.moveStart("character",t),n.select()}},tryParseJson:function(e,t){if(typeof e=="string"&&e.length&&e.match(/^[\{\[].*[\}\]]$/))if(t)try{e=(new Function("return "+e))()}catch(n){}finally{return e}else e=(new Function("return "+e))();return e},sliceObj:function(t,n,r){var i,s,o={};if(!e.isArray(n)||!n.length)return o;for(var u=0;u<n.length;u++){i=n[u],t.hasOwnProperty(i)&&(o[i]=t[i]);if(r===!0)continue;s=i.toLowerCase(),t.hasOwnProperty(s)&&(o[i]=t[s])}return o},getConfigData:function(t){var n={};return e.each(t.data(),function(e,t){if(typeof t!="object"||t&&typeof t=="object"&&(t.constructor===Object||t.constructor===Array))n[e]=t}),n},objectKeys:function(e){if(Object.keys)return Object.keys(e);if(e!==Object(e))throw new TypeError("Object.keys called on a non-object");var t=[],n;for(n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.push(n);return t},escape:function(t){return e("<div>").text(t).html()},itemsByValue:function(t,n,r){if(!n||t===null)return[];r=r||"value";var i=e.isArray(t),s=[],o=this;return e.each(n,function(n,u){u.children?s=s.concat(o.itemsByValue(t,u.children,r)):i?e.grep(t,function(e){return e==(u&&typeof u==="object"?u[r]:u)}).length&&s.push(u):t==(u&&typeof u==="object"?u[r]:u)&&s.push(u)}),s},createInput:function(t){var n,r,i,s=t.type;return s==="date"&&(t.mode==="inline"?e.fn.editabletypes.datefield?s="datefield":e.fn.editabletypes.dateuifield&&(s="dateuifield"):e.fn.editabletypes.date?s="date":e.fn.editabletypes.dateui&&(s="dateui"),s==="date"&&!e.fn.editabletypes.date&&(s="combodate")),s==="datetime"&&t.mode==="inline"&&(s="datetimefield"),s==="wysihtml5"&&!e.fn.editabletypes[s]&&(s="textarea"),typeof e.fn.editabletypes[s]=="function"?(n=e.fn.editabletypes[s],r=this.sliceObj(t,this.objectKeys(n.defaults)),i=new n(r),i):(e.error("Unknown type: "+s),!1)}}}(window.jQuery),function(e){"use strict";var t=function(e,t){this.init(e,t)},n=function(e,t){this.init(e,t)};t.prototype={containerName:null,innerCss:null,containerClass:"editable-container editable-popup",init:function(n,r){this.$element=e(n),this.options=e.extend({},e.fn.editableContainer.defaults,r),this.splitOptions(),this.formOptions.scope=this.$element[0],this.initContainer(),this.$element.on("destroyed",e.proxy(function(){this.destroy()},this)),e(document).data("editable-handlers-attached")||(e(document).on("keyup.editable",function(t){t.which===27&&e(".editable-open").editableContainer("hide")}),e(document).on("click.editable",function(n){var r=e(n.target),i,s=[".editable-container",".ui-datepicker-header",".datepicker",".modal-backdrop",".bootstrap-wysihtml5-insert-image-modal",".bootstrap-wysihtml5-insert-link-modal"];if(!e.contains(document.documentElement,n.target))return;if(r.is(document))return;for(i=0;i<s.length;i++)if(r.is(s[i])||r.parents(s[i]).length)return;t.prototype.closeOthers(n.target)}),e(document).data("editable-handlers-attached",!0))},splitOptions:function(){this.containerOptions={},this.formOptions={};if(!e.fn[this.containerName])throw new Error(this.containerName+" not found. Have you included corresponding js file?");var t=e.fn[this.containerName].defaults;for(var n in this.options)n in t?this.containerOptions[n]=this.options[n]:this.formOptions[n]=this.options[n]},tip:function(){return this.container()?this.container().$tip:null},container:function(){return this.$element.data(this.containerDataName||this.containerName)},call:function(){this.$element[this.containerName].apply(this.$element,arguments)},initContainer:function(){this.call(this.containerOptions)},renderForm:function(){this.$form.editableform(this.formOptions).on({save:e.proxy(this.save,this),nochange:e.proxy(function(){this.hide("nochange")},this),cancel:e.proxy(function(){this.hide("cancel")},this),show:e.proxy(this.setPosition,this),rendering:e.proxy(this.setPosition,this),resize:e.proxy(this.setPosition,this),rendered:e.proxy(function(){this.$element.triggerHandler("shown",this)},this)}).editableform("render")},show:function(t){this.$element.addClass("editable-open"),t!==!1&&this.closeOthers(this.$element[0]),this.innerShow(),this.tip().addClass(this.containerClass),this.$form,this.$form=e("<div>"),this.tip().is(this.innerCss)?this.tip().append(this.$form):this.tip().find(this.innerCss).append(this.$form),this.renderForm()},hide:function(e){if(!this.tip()||!this.tip().is(":visible")||!this.$element.hasClass("editable-open"))return;this.$element.removeClass("editable-open"),this.innerHide(),this.$element.triggerHandler("hidden",e||"manual")},innerShow:function(){},innerHide:function(){},toggle:function(e){this.container()&&this.tip()&&this.tip().is(":visible")?this.hide():this.show(e)},setPosition:function(){},save:function(e,t){this.$element.triggerHandler("save",t),this.hide("save")},option:function(e,t){this.options[e]=t,e in this.containerOptions?(this.containerOptions[e]=t,this.setContainerOption(e,t)):(this.formOptions[e]=t,this.$form&&this.$form.editableform("option",e,t))},setContainerOption:function(e,t){this.call("option",e,t)},destroy:function(){this.hide(),this.innerDestroy(),this.$element.off("destroyed"),this.$element.removeData("editableContainer")},innerDestroy:function(){},closeOthers:function(t){e(".editable-open").each(function(n,r){if(r===t||e(r).find(t).length)return;var i=e(r),s=i.data("editableContainer");if(!s)return;s.options.onblur==="cancel"?i.data("editableContainer").hide("onblur"):s.options.onblur==="submit"&&i.data("editableContainer").tip().find("form").submit()})},activate:function(){this.tip&&this.tip().is(":visible")&&this.$form&&this.$form.data("editableform").input.activate()}},e.fn.editableContainer=function(r){var i=arguments;return this.each(function(){var s=e(this),o="editableContainer",u=s.data(o),a=typeof r=="object"&&r,f=a.mode==="inline"?n:t;u||s.data(o,u=new f(this,a)),typeof r=="string"&&u[r].apply(u,Array.prototype.slice.call(i,1))})},e.fn.editableContainer.Popup=t,e.fn.editableContainer.Inline=n,e.fn.editableContainer.defaults={value:null,placement:"top",autohide:!0,onblur:"cancel",anim:!1,mode:"popup"},jQuery.event.special.destroyed={remove:function(e){e.handler&&e.handler()}}}(window.jQuery),function(e){"use strict";e.extend(e.fn.editableContainer.Inline.prototype,e.fn.editableContainer.Popup.prototype,{containerName:"editableform",innerCss:".editable-inline",containerClass:"editable-container editable-inline",initContainer:function(){this.$tip=e("<span></span>"),this.options.anim||(this.options.anim=0)},splitOptions:function(){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,e.proxy(function(){this.$element.show(),this.innerDestroy()},this))},innerDestroy:function(){this.tip()&&this.tip().empty().remove()}})}(window.jQuery),function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.editable.defaults,n,e.fn.editableutils.getConfigData(this.$element)),this.options.selector?this.initLive():this.init()};t.prototype={constructor:t,init:function(){var t=!1,n,r;this.options.name=this.options.name||this.$element.attr("id"),this.options.scope=this.$element[0],this.input=e.fn.editableutils.createInput(this.options);if(!this.input)return;this.options.value===undefined||this.options.value===null?(this.value=this.input.html2value(e.trim(this.$element.html())),t=!0):(this.options.value=e.fn.editableutils.tryParseJson(this.options.value,!0),typeof this.options.value=="string"?this.value=this.input.str2value(this.options.value):this.value=this.options.value),this.$element.addClass("editable"),this.options.toggle!=="manual"?(this.$element.addClass("editable-click"),this.$element.on(this.options.toggle+".editable",e.proxy(function(e){e.preventDefault();if(this.options.toggle==="mouseenter")this.show();else{var t=this.options.toggle!=="click";this.toggle(t)}},this))):this.$element.attr("tabindex",-1);switch(this.options.autotext){case"always":n=!0;break;case"auto":n=!e.trim(this.$element.text()).length&&this.value!==null&&this.value!==undefined&&!t;break;default:n=!1}e.when(n?this.render():!0).then(e.proxy(function(){this.options.disabled?this.disable():this.enable(),this.$element.triggerHandler("init",this)},this))},initLive:function(){var t=this.options.selector;this.options.selector=!1,this.options.autotext="never",this.$element.on(this.options.toggle+".editable",t,e.proxy(function(t){var n=e(t.target);n.data("editable")||(n.hasClass(this.options.emptyclass)&&n.empty(),n.editable(this.options).trigger(t))},this))},render:function(e){if(this.options.display===!1)return;return this.input.value2htmlFinal?this.input.value2html(this.value,this.$element[0],this.options.display,e):typeof this.options.display=="function"?this.options.display.call(this.$element[0],this.value,e):this.input.value2html(this.value,this.$element[0])},enable:function(){this.options.disabled=!1,this.$element.removeClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.options.toggle!=="manual"&&this.$element.attr("tabindex")==="-1"&&this.$element.removeAttr("tabindex")},disable:function(){this.options.disabled=!0,this.hide(),this.$element.addClass("editable-disabled"),this.handleEmpty(this.isEmpty),this.$element.attr("tabindex",-1)},toggleDisabled:function(){this.options.disabled?this.enable():this.disable()},option:function(t,n){if(t&&typeof t=="object"){e.each(t,e.proxy(function(t,n){this.option(e.trim(t),n)},this));return}this.options[t]=n;if(t==="disabled")return n?this.disable():this.enable();t==="value"&&this.setValue(n),this.container&&this.container.option(t,n),this.input.option&&this.input.option(t,n)},handleEmpty:function(t){if(this.options.display===!1)return;this.isEmpty=t!==undefined?t:e.trim(this.$element.text())==="",this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.text(this.options.emptytext),this.options.emptyclass&&this.$element.addClass(this.options.emptyclass)):this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)},show:function(t){if(this.options.disabled)return;if(!this.container){var n=e.extend({},this.options,{value:this.value,input:this.input});this.$element.editableContainer(n),this.$element.on("save.internal",e.proxy(this.save,this)),this.container=this.$element.data("editableContainer")}else if(this.container.tip().is(":visible"))return;this.container.show(t)},hide:function(){this.container&&this.container.hide()},toggle:function(e){this.container&&this.container.tip().is(":visible")?this.hide():this.show(e)},save:function(e,t){if(this.options.unsavedclass){var n=!1;n=n||typeof this.options.url=="function",n=n||this.options.display===!1,n=n||t.response!==undefined,n=n||this.options.savenochange&&this.input.value2str(this.value)!==this.input.value2str(t.newValue),n?this.$element.removeClass(this.options.unsavedclass):this.$element.addClass(this.options.unsavedclass)}this.setValue(t.newValue,!1,t.response)},validate:function(){if(typeof this.options.validate=="function")return this.options.validate.call(this,this.value)},setValue:function(t,n,r){n?this.value=this.input.str2value(t):this.value=t,this.container&&this.container.option("value",this.value),e.when(this.render(r)).then(e.proxy(function(){this.handleEmpty()},this))},activate:function(){this.container&&this.container.activate()},destroy:function(){this.disable(),this.container&&this.container.destroy(),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")}},e.fn.editable=function(n){var r={},i=arguments,s="editable";switch(n){case"validate":return this.each(function(){var t=e(this),n=t.data(s),i;n&&(i=n.validate())&&(r[n.options.name]=i)}),r;case"getValue":return this.each(function(){var t=e(this),n=t.data(s);n&&n.value!==undefined&&n.value!==null&&(r[n.options.name]=n.input.value2submit(n.value))}),r;case"submit":var o=arguments[1]||{},u=this,a=this.editable("validate"),f;return e.isEmptyObject(a)?(f=this.editable("getValue"),o.data&&e.extend(f,o.data),e.ajax(e.extend({url:o.url,data:f,type:"POST"},o.ajaxOptions)).success(function(e){typeof o.success=="function"&&o.success.call(u,e,o)}).error(function(){typeof o.error=="function"&&o.error.apply(u,arguments)})):typeof o.error=="function"&&o.error.call(u,a),this}return this.each(function(){var r=e(this),o=r.data(s),u=typeof n=="object"&&n;o||r.data(s,o=new t(this,u)),typeof n=="string"&&o[n].apply(o,Array.prototype.slice.call(i,1))})},e.fn.editable.defaults={type:"text",disabled:!1,toggle:"click",emptytext:"Empty",autotext:"auto",value:null,display:null,emptyclass:"editable-empty",unsavedclass:"editable-unsaved",selector:null}}(window.jQuery),function(e){"use strict";e.fn.editabletypes={};var t=function(){};t.prototype={init:function(t,n,r){this.type=t,this.options=e.extend({},r,n)},prerender:function(){this.$tpl=e(this.options.tpl),this.$input=this.$tpl,this.$clear=null,this.error=null},render:function(){},value2html:function(t,n){e(n).text(t)},html2value:function(t){return e("<div>").html(t).text()},value2str:function(e){return e},str2value:function(e){return e},value2submit:function(e){return e},value2input:function(e){this.$input.val(e)},input2value:function(){return this.$input.val()},activate:function(){this.$input.is(":visible")&&this.$input.focus()},clear:function(){this.$input.val(null)},escape:function(t){return e("<div>").text(t).html()},autosubmit:function(){},setClass:function(){this.options.inputclass&&this.$input.addClass(this.options.inputclass)},setAttr:function(e){this.options[e]!==undefined&&this.options[e]!==null&&this.$input.attr(e,this.options[e])},option:function(e,t){this.options[e]=t}},t.defaults={tpl:"",inputclass:"input-medium",scope:null,showbuttons:!0},e.extend(e.fn.editabletypes,{abstractinput:t})}(window.jQuery),function(e){"use strict";var t=function(e){};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){var t=e.Deferred();return this.error=null,this.onSourceReady(function(){this.renderList(),t.resolve()},function(){this.error=this.options.sourceError,t.resolve()}),t.promise()},html2value:function(e){return null},value2html:function(t,n,r,i){var s=e.Deferred(),o=function(){typeof r=="function"?r.call(n,t,this.sourceData,i):this.value2htmlFinal(t,n),s.resolve()};return t===null?o.call(this):this.onSourceReady(o,function(){s.resolve()}),s.promise()},onSourceReady:function(t,n){if(e.isArray(this.sourceData)){t.call(this);return}try{this.options.source=e.fn.editableutils.tryParseJson(this.options.source,!1)}catch(r){n.call(this);return}var i=this.options.source;e.isFunction(i)&&(i=i.call(this.options.scope));if(typeof i=="string"){if(this.options.sourceCache){var s=i,o;e(document).data(s)||e(document).data(s,{}),o=e(document).data(s);if(o.loading===!1&&o.sourceData){this.sourceData=o.sourceData,this.doPrepend(),t.call(this);return}if(o.loading===!0){o.callbacks.push(e.proxy(function(){this.sourceData=o.sourceData,this.doPrepend(),t.call(this)},this)),o.err_callbacks.push(e.proxy(n,this));return}o.loading=!0,o.callbacks=[],o.err_callbacks=[]}e.ajax({url:i,type:"get",cache:!1,dataType:"json",success:e.proxy(function(r){o&&(o.loading=!1),this.sourceData=this.makeArray(r),e.isArray(this.sourceData)?(o&&(o.sourceData=this.sourceData,e.each(o.callbacks,function(){this.call()})),this.doPrepend(),t.call(this)):(n.call(this),o&&e.each(o.err_callbacks,function(){this.call()}))},this),error:e.proxy(function(){n.call(this),o&&(o.loading=!1,e.each(o.err_callbacks,function(){this.call()}))},this)})}else this.sourceData=this.makeArray(i),e.isArray(this.sourceData)?(this.doPrepend(),t.call(this)):n.call(this)},doPrepend:function(){if(this.options.prepend===null||this.options.prepend===undefined)return;e.isArray(this.prependData)||(e.isFunction(this.options.prepend)&&(this.options.prepend=this.options.prepend.call(this.options.scope)),this.options.prepend=e.fn.editableutils.tryParseJson(this.options.prepend,!0),typeof this.options.prepend=="string"&&(this.options.prepend={"":this.options.prepend}),this.prependData=this.makeArray(this.options.prepend)),e.isArray(this.prependData)&&e.isArray(this.sourceData)&&(this.sourceData=this.prependData.concat(this.sourceData))},renderList:function(){},value2htmlFinal:function(e,t){},makeArray:function(t){var n,r,i=[],s,o;if(!t||typeof t=="string")return null;if(e.isArray(t)){o=function(e,t){r={value:e,text:t};if(n++>=2)return!1};for(var u=0;u<t.length;u++)s=t[u],typeof s=="object"?(n=0,e.each(s,o),n===1?i.push(r):n>1&&(s.children&&(s.children=this.makeArray(s.children)),i.push(s))):i.push({value:s,text:s})}else e.each(t,function(e,t){i.push({value:e,text:t})});return i},option:function(e,t){this.options[e]=t,e==="source"&&(this.sourceData=null),e==="prepend"&&(this.prependData=null)}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{source:null,prepend:!1,sourceError:"Error when loading list",sourceCache:!0}),e.fn.editabletypes.list=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("text",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.renderClear(),this.setClass(),this.setAttr("placeholder")},activate:function(){this.$input.is(":visible")&&(this.$input.focus(),e.fn.editableutils.setCursorPosition(this.$input.get(0),this.$input.val().length),this.toggleClear&&this.toggleClear())},renderClear:function(){this.options.clear&&(this.$clear=e('<span class="editable-clear-x"></span>'),this.$input.after(this.$clear).css("padding-right",24).keyup(e.proxy(function(t){if(~e.inArray(t.keyCode,[40,38,9,13,27]))return;clearTimeout(this.t);var n=this;this.t=setTimeout(function(){n.toggleClear(t)},100)},this)).parent().css("position","relative"),this.$clear.click(e.proxy(this.clear,this)))},postrender:function(){},toggleClear:function(e){if(!this.$clear)return;var t=this.$input.val().length,n=this.$clear.is(":visible");t&&!n&&this.$clear.show(),!t&&n&&this.$clear.hide()},clear:function(){this.$clear.hide(),this.$input.val("").focus()}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',placeholder:null,clear:!0}),e.fn.editabletypes.text=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("textarea",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.setAttr("placeholder"),this.setAttr("rows"),this.$input.keydown(function(t){t.ctrlKey&&t.which===13&&e(this).closest("form").submit()})},value2html:function(t,n){var r="",i;if(t){i=t.split("\n");for(var s=0;s<i.length;s++)i[s]=e("<div>").text(i[s]).html();r=i.join("<br>")}e(n).html(r)},html2value:function(t){if(!t)return"";var n=new RegExp(String.fromCharCode(10),"g"),r=t.split(/<br\s*\/?>/i);for(var i=0;i<r.length;i++){var s=e("<div>").html(r[i]).text();s=s.replace(n,""),r[i]=s}return r.join("\n")},activate:function(){e.fn.editabletypes.text.prototype.activate.call(this)}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:"<textarea></textarea>",inputclass:"input-large",placeholder:null,rows:7}),e.fn.editabletypes.textarea=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("select",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.list),e.extend(t.prototype,{renderList:function(){this.$input.empty();var t=function(n,r){if(e.isArray(r))for(var i=0;i<r.length;i++)r[i].children?n.append(t(e("<optgroup>",{label:r[i].text}),r[i].children)):n.append(e("<option>",{value:r[i].value}).text(r[i].text));return n};t(this.$input,this.sourceData),this.setClass(),this.$input.on("keydown.editable",function(t){t.which===13&&e(this).closest("form").submit()})},value2htmlFinal:function(t,n){var r="",i=e.fn.editableutils.itemsByValue(t,this.sourceData);i.length&&(r=i[0].text),e(n).text(r)},autosubmit:function(){this.$input.off("keydown.editable").on("change.editable",function(){e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.list.defaults,{tpl:"<select></select>"}),e.fn.editabletypes.select=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("checklist",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.list),e.extend(t.prototype,{renderList:function(){var t,n;this.$tpl.empty();if(!e.isArray(this.sourceData))return;for(var r=0;r<this.sourceData.length;r++)t=e("<label>").append(e("<input>",{type:"checkbox",value:this.sourceData[r].value})).append(e("<span>").text(" "+this.sourceData[r].text)),e("<div>").append(t).appendTo(this.$tpl);this.$input=this.$tpl.find('input[type="checkbox"]'),this.setClass()},value2str:function(t){return e.isArray(t)?t.sort().join(e.trim(this.options.separator)):""},str2value:function(t){var n,r=null;return typeof t=="string"&&t.length?(n=new RegExp("\\s*"+e.trim(this.options.separator)+"\\s*"),r=t.split(n)):e.isArray(t)?r=t:r=[t],r},value2input:function(t){this.$input.prop("checked",!1),e.isArray(t)&&t.length&&this.$input.each(function(n,r){var i=e(r);e.each(t,function(e,t){i.val()==t&&i.prop("checked",!0)})})},input2value:function(){var t=[];return this.$input.filter(":checked").each(function(n,r){t.push(e(r).val())}),t},value2htmlFinal:function(t,n){var r=[],i=e.fn.editableutils.itemsByValue(t,this.sourceData);i.length?(e.each(i,function(t,n){r.push(e.fn.editableutils.escape(n.text))}),e(n).html(r.join("<br>"))):e(n).empty()},activate:function(){this.$input.first().focus()},autosubmit:function(){this.$input.on("keydown",function(t){t.which===13&&e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.list.defaults,{tpl:'<div class="editable-checklist"></div>',inputclass:null,separator:","}),e.fn.editabletypes.checklist=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("password",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),e.extend(t.prototype,{value2html:function(t,n){t?e(n).text("[hidden]"):e(n).empty()},html2value:function(e){return null}}),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="password">'}),e.fn.editabletypes.password=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("email",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="email">'}),e.fn.editabletypes.email=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("url",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="url">'}),e.fn.editabletypes.url=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("tel",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="tel">'}),e.fn.editabletypes.tel=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("number",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.text),e.extend(t.prototype,{render:function(){t.superclass.render.call(this),this.setAttr("min"),this.setAttr("max"),this.setAttr("step")},postrender:function(){this.$clear&&this.$clear.css({right:24})}}),t.defaults=e.extend({},e.fn.editabletypes.text.defaults,{tpl:'<input type="number">',inputclass:"input-mini",min:null,max:null,step:null}),e.fn.editabletypes.number=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("range",e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.number),e.extend(t.prototype,{render:function(){this.$input=this.$tpl.filter("input"),this.setClass(),this.setAttr("min"),this.setAttr("max"),this.setAttr("step"),this.$input.on("input",function(){e(this).siblings("output").text(e(this).val())})},activate:function(){this.$input.focus()}}),t.defaults=e.extend({},e.fn.editabletypes.number.defaults,{tpl:'<input type="range"><output style="width: 30px; display: inline-block"></output>',inputclass:"input-medium"}),e.fn.editabletypes.range=t}(window.jQuery),function(e){"use strict";var t=function(n){this.init("select2",n,t.defaults),n.select2=n.select2||{};var r=this,i={placeholder:n.placeholder};this.isMultiple=n.select2.tags||n.select2.multiple,n.select2.tags||(n.source&&(i.data=n.source),i.initSelection=function(t,n){var s=r.str2value(t.val()),o=e.fn.editableutils.itemsByValue(s,i.data,"id");e.isArray(o)&&o.length&&!r.isMultiple&&(o=o[0]),n(o)}),this.options.select2=e.extend({},t.defaults.select2,i,n.select2)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.$input.select2(this.options.select2),"ajax"in this.options.select2,this.isMultiple&&this.$input.on("change",function(){e(this).closest("form").parent().triggerHandler("resize")})},value2html:function(t,n){var r="",i;this.$input?i=this.$input.select2("data"):this.options.select2.tags?i=t:this.options.select2.data&&(i=e.fn.editableutils.itemsByValue(t,this.options.select2.data,"id")),e.isArray(i)?(r=[],e.each(i,function(e,t){r.push(t&&typeof t=="object"?t.text:t)})):i&&(r=i.text),r=e.isArray(r)?r.join(this.options.viewseparator):r,e(n).text(r)},html2value:function(e){return this.options.select2.tags?this.str2value(e,this.options.viewseparator):null},value2input:function(e){this.$input.val(e).trigger("change",!0)},input2value:function(){return this.$input.select2("val")},str2value:function(t,n){if(typeof t!="string"||!this.isMultiple)return t;n=n||this.options.select2.separator||e.fn.select2.defaults.separator;var r,i,s;if(t===null||t.length<1)return null;r=t.split(n);for(i=0,s=r.length;i<s;i+=1)r[i]=e.trim(r[i]);return r},autosubmit:function(){this.$input.on("change",function(t,n){n||e(this).closest("form").submit()})}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="hidden">',select2:null,placeholder:null,source:null,viewseparator:", "}),e.fn.editabletypes.select2=t}(window.jQuery),function(e){var t=function(t,n){this.$element=e(t);if(!this.$element.is("input")){e.error("Combodate should be applied to INPUT element");return}this.options=e.extend({},e.fn.combodate.defaults,n,this.$element.data()),this.init()};t.prototype={constructor:t,init:function(){this.map={day:["D","date"],month:["M","month"],year:["Y","year"],hour:["[Hh]","hours"],minute:["m","minutes"],second:["s","seconds"],ampm:["[Aa]",""]},this.$widget=e('<span class="combodate"></span>').html(this.getTemplate()),this.initCombos(),this.$widget.on("change","select",e.proxy(function(){this.$element.val(this.getValue())},this)),this.$widget.find("select").css("width","auto"),this.$element.hide().after(this.$widget),this.setValue(this.$element.val()||this.options.value)},getTemplate:function(){var t=this.options.template;return e.each(this.map,function(e,n){n=n[0];var r=new RegExp(n+"+"),i=n.length>1?n.substring(1,2):n;t=t.replace(r,"{"+i+"}")}),t=t.replace(/ /g," "),e.each(this.map,function(e,n){n=n[0];var r=n.length>1?n.substring(1,2):n;t=t.replace("{"+r+"}",'<select class="'+e+'"></select>')}),t},initCombos:function(){var t=this;e.each(this.map,function(e,n){var r=t.$widget.find("."+e),i,s;r.length&&(t["$"+e]=r,i="fill"+e.charAt(0).toUpperCase()+e.slice(1),s=t[i](),t["$"+e].html(t.renderItems(s)))})},initItems:function(e){var t=[],n;if(this.options.firstItem==="name"){n=moment.relativeTime||moment.langData()._relativeTime;var r=typeof n[e]=="function"?n[e](1,!0,e,!1):n[e];r=r.split(" ").reverse()[0],t.push(["",r])}else this.options.firstItem==="empty"&&t.push(["",""]);return t},renderItems:function(e){var t=[];for(var n=0;n<e.length;n++)t.push('<option value="'+e[n][0]+'">'+e[n][1]+"</option>");return t.join("\n")},fillDay:function(){var e=this.initItems("d"),t,n,r=this.options.template.indexOf("DD")!==-1;for(n=1;n<=31;n++)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillMonth:function(){var e=this.initItems("M"),t,n,r=this.options.template.indexOf("MMMM")!==-1,i=this.options.template.indexOf("MMM")!==-1,s=this.options.template.indexOf("MM")!==-1;for(n=0;n<=11;n++)r?t=moment().month(n).format("MMMM"):i?t=moment().month(n).format("MMM"):s?t=this.leadZero(n+1):t=n+1,e.push([n,t]);return e},fillYear:function(){var e=[],t,n,r=this.options.template.indexOf("YYYY")!==-1;for(n=this.options.maxYear;n>=this.options.minYear;n--)t=r?n:(n+"").substring(2),e[this.options.yearDescending?"push":"unshift"]([n,t]);return e=this.initItems("y").concat(e),e},fillHour:function(){var e=this.initItems("h"),t,n,r=this.options.template.indexOf("h")!==-1,i=this.options.template.indexOf("H")!==-1,s=this.options.template.toLowerCase().indexOf("hh")!==-1,o=r?12:23;for(n=0;n<=o;n++)t=s?this.leadZero(n):n,e.push([n,t]);return e},fillMinute:function(){var e=this.initItems("m"),t,n,r=this.options.template.indexOf("mm")!==-1;for(n=0;n<=59;n+=this.options.minuteStep)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillSecond:function(){var e=this.initItems("s"),t,n,r=this.options.template.indexOf("ss")!==-1;for(n=0;n<=59;n+=this.options.secondStep)t=r?this.leadZero(n):n,e.push([n,t]);return e},fillAmpm:function(){var e=this.options.template.indexOf("a")!==-1,t=this.options.template.indexOf("A")!==-1,n=[["am",e?"am":"AM"],["pm",e?"pm":"PM"]];return n},getValue:function(t){var n,r={},i=this,s=!1;return e.each(this.map,function(e,t){if(e==="ampm")return;var n=e==="day"?1:0;r[e]=i["$"+e]?parseInt(i["$"+e].val(),10):n;if(isNaN(r[e]))return s=!0,!1}),s?"":(this.$ampm&&(r.hour=this.$ampm.val()==="am"?r.hour:r.hour+12,r.hour===24&&(r.hour=0)),n=moment([r.year,r.month,r.day,r.hour,r.minute,r.second]),this.highlight(n),t=t===undefined?this.options.format:t,t===null?n.isValid()?n:null:n.isValid()?n.format(t):"")},setValue:function(t){function s(t,n){var r={};return t.children("option").each(function(t,i){var s=e(i).attr("value"),o;if(s==="")return;o=Math.abs(s-n);if(typeof r.distance=="undefined"||o<r.distance)r={value:s,distance:o}}),r.value}if(!t)return;var n=typeof t=="string"?moment(t,this.options.format):moment(t),r=this,i={};n.isValid()&&(e.each(this.map,function(e,t){if(e==="ampm")return;i[e]=n[t[1]]()}),this.$ampm&&(i.hour>12?(i.hour-=12,i.ampm="pm"):i.ampm="am"),e.each(i,function(e,t){r["$"+e]&&(e==="minute"&&r.options.minuteStep>1&&r.options.roundTime&&(t=s(r["$"+e],t)),e==="second"&&r.options.secondStep>1&&r.options.roundTime&&(t=s(r["$"+e],t)),r["$"+e].val(t))}),this.$element.val(n.format(this.options.format)))},highlight:function(e){e.isValid()?this.options.errorClass?this.$widget.removeClass(this.options.errorClass):this.$widget.find("select").css("border-color",this.borderColor):this.options.errorClass?this.$widget.addClass(this.options.errorClass):(this.borderColor||(this.borderColor=this.$widget.find("select").css("border-color")),this.$widget.find("select").css("border-color","red"))},leadZero:function(e){return e<=9?"0"+e:e},destroy:function(){this.$widget.remove(),this.$element.removeData("combodate").show()}},e.fn.combodate=function(n){var r,i=Array.apply(null,arguments);return i.shift(),n==="getValue"&&this.length&&(r=this.eq(0).data("combodate"))?r.getValue.apply(r,i):this.each(function(){var r=e(this),s=r.data("combodate"),o=typeof n=="object"&&n;s||r.data("combodate",s=new t(this,o)),typeof n=="string"&&typeof s[n]=="function"&&s[n].apply(s,i)})},e.fn.combodate.defaults={format:"DD-MM-YYYY HH:mm",template:"D / MMM / YYYY H : mm",value:null,minYear:1970,maxYear:2015,yearDescending:!0,minuteStep:5,secondStep:1,firstItem:"empty",errorClass:null,roundTime:!0}}(window.jQuery),function(e){"use strict";var t=function(n){this.init("combodate",n,t.defaults),this.options.viewformat||(this.options.viewformat=this.options.format),n.combodate=e.fn.editableutils.tryParseJson(n.combodate,!0),this.options.combodate=e.extend({},t.defaults.combodate,n.combodate,{format:this.options.format,template:this.options.template})};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.$input.combodate(this.options.combodate)},value2html:function(t,n){var r=t?t.format(this.options.viewformat):"";e(n).text(r)},html2value:function(e){return e?moment(e,this.options.viewformat):null},value2str:function(e){return e?e.format(this.options.format):""},str2value:function(e){return e?moment(e,this.options.format):null},value2submit:function(e){return this.value2str(e)},value2input:function(e){this.$input.combodate("setValue",e)},input2value:function(){return this.$input.combodate("getValue",null)},activate:function(){this.$input.siblings(".combodate").find("select").eq(0).focus()},autosubmit:function(){}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<input type="text">',inputclass:null,format:"YYYY-MM-DD",viewformat:null,template:"D / MMM / YYYY",combodate:null}),e.fn.editabletypes.combodate=t}(window.jQuery),function(e){"use strict";e.extend(e.fn.editableform.Constructor.prototype,{initButtons:function(){var t=this.$form.find(".editable-buttons");t.append(e.fn.editableform.buttons),this.options.showbuttons==="bottom"&&t.addClass("editable-buttons-bottom"),this.$form.find(".editable-submit").button({icons:{primary:"ui-icon-check"},text:!1}).removeAttr("title"),this.$form.find(".editable-cancel").button({icons:{primary:"ui-icon-closethick"},text:!1}).removeAttr("title")}}),e.fn.editableform.errorGroupClass=null,e.fn.editableform.errorBlockClass="ui-state-error"}(window.jQuery),function(e){"use strict";e.extend(e.fn.editableContainer.Popup.prototype,{containerName:"tooltip",containerDataName:"uiTooltip",innerCss:".ui-tooltip-content",splitOptions:function(){this.containerOptions={},this.formOptions={};var t=e.ui[this.containerName].prototype.options;for(var n in this.options)n in t?this.containerOptions[n]=this.options[n]:this.formOptions[n]=this.options[n]},initContainer:function(){this.handlePlacement(),e.extend(this.containerOptions,{items:"*",content:" ",track:!1,open:e.proxy(function(){this.container()._on(this.container().element,{mouseleave:function(e){e.stopImmediatePropagation()},focusout:function(e){e.stopImmediatePropagation()}})},this)}),this.call(this.containerOptions),this.container()._off(this.container().element,"mouseover focusin")},tip:function(){return this.container()?this.container()._find(this.container().element):null},innerShow:function(){this.call("open");var t=this.options.title||this.$element.data("ui-tooltip-title")||this.$element.data("originalTitle");this.tip().find(this.innerCss).empty().append(e("<label>").text(t))},innerHide:function(){this.call("close")},innerDestroy:function(){},setPosition:function(){this.tip().position(e.extend({of:this.$element},this.containerOptions.position))},handlePlacement:function(){var e;switch(this.options.placement){case"top":e={my:"center bottom-5",at:"center top"};break;case"right":e={my:"left+5 center",at:"right center"};break;case"bottom":e={my:"center top+5",at:"center bottom"};break;case"left":e={my:"right-5 center",at:"left center"}}this.containerOptions.position=e}})}(window.jQuery),function(e){"use strict";var t=function(e){this.init("dateui",e,t.defaults),this.initPicker(e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{initPicker:function(t,n){this.options.viewformat||(this.options.viewformat=this.options.format),this.options.viewformat=this.options.viewformat.replace("yyyy","yy"),this.options.format=this.options.format.replace("yyyy","yy"),this.options.datepicker=e.extend({},n.datepicker,t.datepicker,{dateFormat:this.options.viewformat})},render:function(){this.$input.datepicker(this.options.datepicker),this.options.clear&&(this.$clear=e('<a href="#"></a>').html(this.options.clear).click(e.proxy(function(e){e.preventDefault(),e.stopPropagation(),this.clear()},this)),this.$tpl.parent().append(e('<div class="editable-clear">').append(this.$clear)))},value2html:function(n,r){var i=e.datepicker.formatDate(this.options.viewformat,n);t.superclass.value2html(i,r)},html2value:function(t){if(typeof t!="string")return t;var n;try{n=e.datepicker.parseDate(this.options.viewformat,t)}catch(r){}return n},value2str:function(t){return e.datepicker.formatDate(this.options.format,t)},str2value:function(t){if(typeof t!="string")return t;var n;try{n=e.datepicker.parseDate(this.options.format,t)}catch(r){}return n},value2submit:function(e){return this.value2str(e)},value2input:function(e){this.$input.datepicker("setDate",e)},input2value:function(){return this.$input.datepicker("getDate")},activate:function(){},clear:function(){this.$input.datepicker("setDate",null)},autosubmit:function(){this.$input.on("mouseup","table.ui-datepicker-calendar a.ui-state-default",function(t){var n=e(this).closest("form");setTimeout(function(){n.submit()},200)})}}),t.defaults=e.extend({},e.fn.editabletypes.abstractinput.defaults,{tpl:'<div class="editable-date"></div>',inputclass:null,format:"yyyy-mm-dd",viewformat:null,datepicker:{firstDay:0,changeYear:!0,changeMonth:!0,showOtherMonths:!0},clear:"× clear"}),e.fn.editabletypes.dateui=t}(window.jQuery),function(e){"use strict";var t=function(e){this.init("dateuifield",e,t.defaults),this.initPicker(e,t.defaults)};e.fn.editableutils.inherit(t,e.fn.editabletypes.dateui),e.extend(t.prototype,{render:function(){this.$input.datepicker(this.options.datepicker),e.fn.editabletypes.text.prototype.renderClear.call(this)},value2input:function(t){this.$input.val(e.datepicker.formatDate(this.options.viewformat,t))},input2value:function(){return this.html2value(this.$input.val())},activate:function(){e.fn.editabletypes.text.prototype.activate.call(this)},toggleClear:function(){e.fn.editabletypes.text.prototype.toggleClear.call(this)},autosubmit:function(){}}),t.defaults=e.extend({},e.fn.editabletypes.dateui.defaults,{tpl:'<input type="text"/>',inputclass:null,datepicker:{showOn:"button",buttonImage:"http://jqueryui.com/resources/demos/datepicker/images/calendar.gif",buttonImageOnly:!0,firstDay:0,changeYear:!0,changeMonth:!0,showOtherMonths:!0},clear:!1}),e.fn.editabletypes.dateuifield=t}(window.jQuery); \ No newline at end of file