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/CHANGELOG.txt b/CHANGELOG.txt index 3c86e5e..82459d2 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,26 @@ X-editable changelog ============================= +Version 1.4.5 Jun 23, 2013 +---------------------------- +[enh #245] highlight element after update (vitalets) +[enh] select2 now works with ajax source (vitalets) +[bug] fix datefield (datetimefield) to return null for incorrect dates (vitalets) +[bug #224] do not close popup when it is saving value (vitalets) +[enh] added `submitValue` to `save` event params (vitalets) +[enh #259] allow `getValue` method to return value itself, not object (vitalets) +[enh] add `destroy` method to inputs (vitalets) +[enh #164] allow emptytext to be html (vitalets) +[enh #78] allow html in editable content (vitalets) +[enh] update container position when datetimepicker viewMode changes (vitalets) +[enh #255] remove xxxView options from first level config of datetimepicker (vitalets) +[enh] if `display` defined as function call it on init (vitalets) +[enh #218] sourceCache now disables cache totally (vitalets) +[bug #156] solve conflict of bootstrap datepicker and jQuery UI datepicker (vitalets) +[enh] update bootstrap-datepicker to 1.1.2 (vitalets) +[enh] allow follow links in disabled state (vitalets) +[enh] update combodate to 1.0.4, fix #222 (vitalets) + Version 1.4.4 May 4, 2013 ---------------------------- diff --git a/Package.nuspec b/Package.nuspec index de6c5df..a5fdeb0 100644 --- a/Package.nuspec +++ b/Package.nuspec @@ -2,14 +2,14 @@ <package > <metadata> <id>x-editable</id> - <version>1.4.4</version> + <version>1.4.5</version> <authors>Vitaliy Potapov</authors> <owners>Vitaliy Potapov</owners> <licenseUrl>https://github.com/vitalets/x-editable/blob/master/LICENSE-MIT</licenseUrl> <projectUrl>http://vitalets.github.com/x-editable</projectUrl> <requireLicenseAcceptance>false</requireLicenseAcceptance> <description>In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery</description> - <releaseNotes>Bootstrap datetimepicker and more</releaseNotes> + <releaseNotes>Select2 with ajax source and more</releaseNotes> <copyright>Copyright 2012-2013</copyright> <tags>edit-in-place editable bootstrap jquery</tags> <dependencies> diff --git a/README.md b/README.md index 18f0d7e..2f7024f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,35 @@ # 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** +## Live demo +**http://vitalets.github.io/x-editable/demo.html** + +## Installation + +### Manual download +Use **http://vitalets.github.io/x-editable** + +### Bower +```` +bower install x-editable +```` + +### CDN +````js +<link href="//cdnjs.cloudflare.com/ajax/libs/x-editable/1.4.5/bootstrap-editable/css/bootstrap-editable.css" rel="stylesheet"/> +<script src="//cdnjs.cloudflare.com/ajax/libs/x-editable/1.4.5/bootstrap-editable/js/bootstrap-editable.min.js"></script> +```` + +## Documentation +**http://vitalets.github.io/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) +When creating issues please provide [jsFiddle](http://jsfiddle.net) example. You can easily fork one of following: +1. [jsFiddle bootstrap template](http://jsfiddle.net/xBB5x/195) +2. [jsFiddle jqueryui template](http://jsfiddle.net/xBB5x/196) +3. [jsFiddle jquery template](http://jsfiddle.net/xBB5x/197) Your feedback is very appreciated! ## Contribution @@ -68,4 +87,4 @@ Thanks for your support! ## License Copyright (c) 2012 Vitaliy Potapov -Licensed under the MIT licenses. \ No newline at end of file +Licensed under the MIT license. \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..6204084 --- /dev/null +++ b/bower.json @@ -0,0 +1,16 @@ +{ + "name": "x-editable", + "version": "1.4.5", + "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..82459d2 --- /dev/null +++ b/dist/CHANGELOG.txt @@ -0,0 +1,201 @@ +X-editable changelog +============================= + +Version 1.4.5 Jun 23, 2013 +---------------------------- +[enh #245] highlight element after update (vitalets) +[enh] select2 now works with ajax source (vitalets) +[bug] fix datefield (datetimefield) to return null for incorrect dates (vitalets) +[bug #224] do not close popup when it is saving value (vitalets) +[enh] added `submitValue` to `save` event params (vitalets) +[enh #259] allow `getValue` method to return value itself, not object (vitalets) +[enh] add `destroy` method to inputs (vitalets) +[enh #164] allow emptytext to be html (vitalets) +[enh #78] allow html in editable content (vitalets) +[enh] update container position when datetimepicker viewMode changes (vitalets) +[enh #255] remove xxxView options from first level config of datetimepicker (vitalets) +[enh] if `display` defined as function call it on init (vitalets) +[enh #218] sourceCache now disables cache totally (vitalets) +[bug #156] solve conflict of bootstrap datepicker and jQuery UI datepicker (vitalets) +[enh] update bootstrap-datepicker to 1.1.2 (vitalets) +[enh] allow follow links in disabled state (vitalets) +[enh] update combodate to 1.0.4, fix #222 (vitalets) + + +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..2f7024f --- /dev/null +++ b/dist/README.md @@ -0,0 +1,90 @@ +# X-editable + +In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery. + +## Live demo +**http://vitalets.github.io/x-editable/demo.html** + +## Installation + +### Manual download +Use **http://vitalets.github.io/x-editable** + +### Bower +```` +bower install x-editable +```` + +### CDN +````js +<link href="//cdnjs.cloudflare.com/ajax/libs/x-editable/1.4.5/bootstrap-editable/css/bootstrap-editable.css" rel="stylesheet"/> +<script src="//cdnjs.cloudflare.com/ajax/libs/x-editable/1.4.5/bootstrap-editable/js/bootstrap-editable.min.js"></script> +```` + +## Documentation +**http://vitalets.github.io/x-editable** + + +## Reporting issues +When creating issues please provide [jsFiddle](http://jsfiddle.net) example. You can easily fork one of following: +1. [jsFiddle bootstrap template](http://jsfiddle.net/xBB5x/195) +2. [jsFiddle jqueryui template](http://jsfiddle.net/xBB5x/196) +3. [jsFiddle jquery template](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 license. \ 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..35e8e1e --- /dev/null +++ b/dist/bootstrap-editable/css/bootstrap-editable.css @@ -0,0 +1,651 @@ +/*! X-editable - v1.4.5 +* 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: '*'*/ +} + +.editable-bg-transition { + -webkit-transition: background-color 1400ms ease-out; + -moz-transition: background-color 1400ms ease-out; + -o-transition: background-color 1400ms ease-out; + -ms-transition: background-color 1400ms ease-out; + transition: background-color 1400ms ease-out; +} + +/*see https://github.com/vitalets/x-editable/issues/139 */ +.form-horizontal .editable +{ + padding-top: 5px; + display:inline-block; +} + + +/*! + * Datepicker for Bootstrap + * + * Copyright 2012 Stefan Petre + * Improvements by Andrew Rowls + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + */ +.datepicker { + padding: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + direction: ltr; + /*.dow { + border-top: 1px solid #ddd !important; + }*/ + +} +.datepicker-inline { + width: 220px; +} +.datepicker.datepicker-rtl { + direction: rtl; +} +.datepicker.datepicker-rtl table tr td span { + float: right; +} +.datepicker-dropdown { + top: 0; + left: 0; +} +.datepicker-dropdown:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 6px; +} +.datepicker-dropdown:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 7px; +} +.datepicker > div { + display: none; +} +.datepicker.days div.datepicker-days { + display: block; +} +.datepicker.months div.datepicker-months { + display: block; +} +.datepicker.years div.datepicker-years { + display: block; +} +.datepicker table { + margin: 0; +} +.datepicker td, +.datepicker th { + text-align: center; + width: 20px; + height: 20px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border: none; +} +.table-striped .datepicker table tr td, +.table-striped .datepicker table tr th { + background-color: transparent; +} +.datepicker table tr td.day:hover { + background: #eeeeee; + cursor: pointer; +} +.datepicker table tr td.old, +.datepicker table tr td.new { + color: #999999; +} +.datepicker table tr td.disabled, +.datepicker table tr td.disabled:hover { + background: none; + color: #999999; + cursor: default; +} +.datepicker table tr td.today, +.datepicker table tr td.today:hover, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today.disabled:hover { + background-color: #fde19a; + background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a)); + background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -o-linear-gradient(top, #fdd49a, #fdf59a); + background-image: linear-gradient(top, #fdd49a, #fdf59a); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0); + border-color: #fdf59a #fdf59a #fbed50; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #000; +} +.datepicker table tr td.today:hover, +.datepicker table tr td.today:hover:hover, +.datepicker table tr td.today.disabled:hover, +.datepicker table tr td.today.disabled:hover:hover, +.datepicker table tr td.today:active, +.datepicker table tr td.today:hover:active, +.datepicker table tr td.today.disabled:active, +.datepicker table tr td.today.disabled:hover:active, +.datepicker table tr td.today.active, +.datepicker table tr td.today:hover.active, +.datepicker table tr td.today.disabled.active, +.datepicker table tr td.today.disabled:hover.active, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today:hover.disabled, +.datepicker table tr td.today.disabled.disabled, +.datepicker table tr td.today.disabled:hover.disabled, +.datepicker table tr td.today[disabled], +.datepicker table tr td.today:hover[disabled], +.datepicker table tr td.today.disabled[disabled], +.datepicker table tr td.today.disabled:hover[disabled] { + background-color: #fdf59a; +} +.datepicker table tr td.today:active, +.datepicker table tr td.today:hover:active, +.datepicker table tr td.today.disabled:active, +.datepicker table tr td.today.disabled:hover:active, +.datepicker table tr td.today.active, +.datepicker table tr td.today:hover.active, +.datepicker table tr td.today.disabled.active, +.datepicker table tr td.today.disabled:hover.active { + background-color: #fbf069 \9; +} +.datepicker table tr td.today:hover:hover { + color: #000; +} +.datepicker table tr td.today.active:hover { + color: #fff; +} +.datepicker table tr td.range, +.datepicker table tr td.range:hover, +.datepicker table tr td.range.disabled, +.datepicker table tr td.range.disabled:hover { + background: #eeeeee; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.datepicker table tr td.range.today, +.datepicker table tr td.range.today:hover, +.datepicker table tr td.range.today.disabled, +.datepicker table tr td.range.today.disabled:hover { + background-color: #f3d17a; + background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a)); + background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -o-linear-gradient(top, #f3c17a, #f3e97a); + background-image: linear-gradient(top, #f3c17a, #f3e97a); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0); + border-color: #f3e97a #f3e97a #edde34; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.datepicker table tr td.range.today:hover, +.datepicker table tr td.range.today:hover:hover, +.datepicker table tr td.range.today.disabled:hover, +.datepicker table tr td.range.today.disabled:hover:hover, +.datepicker table tr td.range.today:active, +.datepicker table tr td.range.today:hover:active, +.datepicker table tr td.range.today.disabled:active, +.datepicker table tr td.range.today.disabled:hover:active, +.datepicker table tr td.range.today.active, +.datepicker table tr td.range.today:hover.active, +.datepicker table tr td.range.today.disabled.active, +.datepicker table tr td.range.today.disabled:hover.active, +.datepicker table tr td.range.today.disabled, +.datepicker table tr td.range.today:hover.disabled, +.datepicker table tr td.range.today.disabled.disabled, +.datepicker table tr td.range.today.disabled:hover.disabled, +.datepicker table tr td.range.today[disabled], +.datepicker table tr td.range.today:hover[disabled], +.datepicker table tr td.range.today.disabled[disabled], +.datepicker table tr td.range.today.disabled:hover[disabled] { + background-color: #f3e97a; +} +.datepicker table tr td.range.today:active, +.datepicker table tr td.range.today:hover:active, +.datepicker table tr td.range.today.disabled:active, +.datepicker table tr td.range.today.disabled:hover:active, +.datepicker table tr td.range.today.active, +.datepicker table tr td.range.today:hover.active, +.datepicker table tr td.range.today.disabled.active, +.datepicker table tr td.range.today.disabled:hover.active { + background-color: #efe24b \9; +} +.datepicker table tr td.selected, +.datepicker table tr td.selected:hover, +.datepicker table tr td.selected.disabled, +.datepicker table tr td.selected.disabled:hover { + background-color: #9e9e9e; + background-image: -moz-linear-gradient(top, #b3b3b3, #808080); + background-image: -ms-linear-gradient(top, #b3b3b3, #808080); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080)); + background-image: -webkit-linear-gradient(top, #b3b3b3, #808080); + background-image: -o-linear-gradient(top, #b3b3b3, #808080); + background-image: linear-gradient(top, #b3b3b3, #808080); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0); + border-color: #808080 #808080 #595959; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td.selected:hover, +.datepicker table tr td.selected:hover:hover, +.datepicker table tr td.selected.disabled:hover, +.datepicker table tr td.selected.disabled:hover:hover, +.datepicker table tr td.selected:active, +.datepicker table tr td.selected:hover:active, +.datepicker table tr td.selected.disabled:active, +.datepicker table tr td.selected.disabled:hover:active, +.datepicker table tr td.selected.active, +.datepicker table tr td.selected:hover.active, +.datepicker table tr td.selected.disabled.active, +.datepicker table tr td.selected.disabled:hover.active, +.datepicker table tr td.selected.disabled, +.datepicker table tr td.selected:hover.disabled, +.datepicker table tr td.selected.disabled.disabled, +.datepicker table tr td.selected.disabled:hover.disabled, +.datepicker table tr td.selected[disabled], +.datepicker table tr td.selected:hover[disabled], +.datepicker table tr td.selected.disabled[disabled], +.datepicker table tr td.selected.disabled:hover[disabled] { + background-color: #808080; +} +.datepicker table tr td.selected:active, +.datepicker table tr td.selected:hover:active, +.datepicker table tr td.selected.disabled:active, +.datepicker table tr td.selected.disabled:hover:active, +.datepicker table tr td.selected.active, +.datepicker table tr td.selected:hover.active, +.datepicker table tr td.selected.disabled.active, +.datepicker table tr td.selected.disabled:hover.active { + background-color: #666666 \9; +} +.datepicker table tr td.active, +.datepicker table tr td.active:hover, +.datepicker table tr td.active.disabled, +.datepicker table tr td.active.disabled:hover { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td.active:hover, +.datepicker table tr td.active:hover:hover, +.datepicker table tr td.active.disabled:hover, +.datepicker table tr td.active.disabled:hover:hover, +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active, +.datepicker table tr td.active.disabled, +.datepicker table tr td.active:hover.disabled, +.datepicker table tr td.active.disabled.disabled, +.datepicker table tr td.active.disabled:hover.disabled, +.datepicker table tr td.active[disabled], +.datepicker table tr td.active:hover[disabled], +.datepicker table tr td.active.disabled[disabled], +.datepicker table tr td.active.disabled:hover[disabled] { + background-color: #0044cc; +} +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active { + background-color: #003399 \9; +} +.datepicker table tr td span { + display: block; + width: 23%; + height: 54px; + line-height: 54px; + float: left; + margin: 1%; + cursor: pointer; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.datepicker table tr td span:hover { + background: #eeeeee; +} +.datepicker table tr td span.disabled, +.datepicker table tr td span.disabled:hover { + background: none; + color: #999999; + cursor: default; +} +.datepicker table tr td span.active, +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active.disabled, +.datepicker table tr td span.active.disabled:hover { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active:hover:hover, +.datepicker table tr td span.active.disabled:hover, +.datepicker table tr td span.active.disabled:hover:hover, +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active, +.datepicker table tr td span.active.disabled, +.datepicker table tr td span.active:hover.disabled, +.datepicker table tr td span.active.disabled.disabled, +.datepicker table tr td span.active.disabled:hover.disabled, +.datepicker table tr td span.active[disabled], +.datepicker table tr td span.active:hover[disabled], +.datepicker table tr td span.active.disabled[disabled], +.datepicker table tr td span.active.disabled:hover[disabled] { + background-color: #0044cc; +} +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active { + background-color: #003399 \9; +} +.datepicker table tr td span.old, +.datepicker table tr td span.new { + color: #999999; +} +.datepicker th.datepicker-switch { + width: 145px; +} +.datepicker thead tr:first-child th, +.datepicker tfoot tr th { + cursor: pointer; +} +.datepicker thead tr:first-child th:hover, +.datepicker tfoot tr th:hover { + background: #eeeeee; +} +.datepicker .cw { + font-size: 10px; + width: 12px; + padding: 0 2px 0 5px; + vertical-align: middle; +} +.datepicker thead tr:first-child th.cw { + cursor: default; + background-color: transparent; +} +.input-append.date .add-on i, +.input-prepend.date .add-on i { + display: block; + cursor: pointer; + width: 16px; + height: 16px; +} +.input-daterange input { + text-align: center; +} +.input-daterange input:first-child { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-daterange input:last-child { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-daterange .add-on { + display: inline-block; + width: auto; + min-width: 16px; + height: 18px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + vertical-align: middle; + background-color: #eeeeee; + border: 1px solid #ccc; + margin-left: -5px; + margin-right: -5px; +} diff --git a/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..3aa27c1 --- /dev/null +++ b/dist/bootstrap-editable/js/bootstrap-editable.js @@ -0,0 +1,6621 @@ +/*! X-editable - v1.4.5 +* 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(); + + //flag showing is form now saving value to server. + //It is needed to wait when closing form. + this.isSaving = false; + + /** + Fired when rendering starts + @event rendering + @param {Object} event event object + **/ + this.$div.triggerHandler('rendering'); + + //init input + this.initInput(); + + //append input to form + this.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; + } + + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + this.isSaving = true; + + //sending data to server + $.when(this.save(submitValue)) + .done($.proxy(function(response) { + this.isSaving = false; + + //run success callback + var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; + + //if success callback returns false --> keep form open and do not activate input + if(res === false) { + this.error(false); + this.showForm(false); + return; + } + + //if success callback returns string --> keep form open, show error and activate input + if(typeof res === 'string') { + this.error(res); + this.showForm(); + return; + } + + //if success callback returns object like {newValue: <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 raw new value + @param {mixed} params.submitValue submitted value as string + @param {Object} params.response ajax response + + @example + $('#form-div').on('save'), function(e, params){ + if(params.newValue === 'username') {...} + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); + }, this)) + .fail($.proxy(function(xhr) { + this.isSaving = false; + + var msg; + if(typeof this.options.error === 'function') { + msg = this.options.error.call(this.options.scope, xhr, newValue); + } else { + msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; + } + + this.error(msg); + this.showForm(); + }, this)); + }, + + save: function(submitValue) { + //try parse composite pk defined as json string in data-pk + this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); + + var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, + /* + send on server in following cases: + 1. url is function + 2. url is string AND (pk defined OR send option = always) + */ + send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), + params; + + if (send) { //send to server + this.showLoading(); + + //standard params + params = { + name: this.options.name || '', + value: submitValue, + pk: pk + }; + + //additional params + if(typeof this.options.params === 'function') { + params = this.options.params.call(this.options.scope, params); + } else { + //try parse json in single quotes (from data-params attribute) + this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); + $.extend(params, this.options.params); + } + + if(typeof this.options.url === 'function') { //user's function + return this.options.url.call(this.options.scope, params); + } else { + //send ajax to server and return deferred object + return $.ajax($.extend({ + url : this.options.url, + data : params, + type : 'POST' + }, this.options.ajaxOptions)); + } + } + }, + + validate: function (value) { + if (value === undefined) { + value = this.value; + } + if (typeof this.options.validate === 'function') { + return this.options.validate.call(this.options.scope, value); + } + }, + + option: function(key, value) { + if(key in this.options) { + this.options[key] = value; + } + + if(key === 'value') { + this.setValue(value); + } + + //do not pass option to input as it is passed in editable-element + }, + + setValue: function(value, convertStr) { + if(convertStr) { + this.value = this.input.str2value(value); + } else { + this.value = value; + } + + //if form is visible, update input + if(this.$form && this.$form.is(':visible')) { + this.input.value2input(this.value); + } + } + }; + + /* + Initialize editableform. Applied to jQuery object. + + @method $().editableform(options) + @params {Object} options + @example + var $form = $('<div>').editableform({ + type: 'text', + name: 'username', + url: '/post', + value: 'vitaliy' + }); + + //to display form you should call 'render' method + $form.editableform('render'); + */ + $.fn.editableform = function (option) { + var args = arguments; + return this.each(function () { + var $this = $(this), + data = $this.data('editableform'), + options = typeof option === 'object' && option; + if (!data) { + $this.data('editableform', (data = new EditableForm(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + //keep link to constructor to allow inheritance + $.fn.editableform.Constructor = EditableForm; + + //defaults + $.fn.editableform.defaults = { + /* see also defaults for input */ + + /** + Type of input. Can be <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; + } + }, + + //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr + supportsTransitions: function () { + var b = document.body || document.documentElement, + s = b.style, + p = 'transition', + v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; + + if(typeof s[p] === 'string') { + return true; + } + + // Tests for vendor specific prop + p = p.charAt(0).toUpperCase() + p.substr(1); + for(var i=0; i<v.length; i++) { + if(typeof s[v[i] + p] === 'string') { + return true; + } + } + 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(); + + //flag to hide container, when saving value will finish + this.delayedHide = false; + + //bind 'destroyed' listener to destroy container when element is removed from dom + this.$element.on('destroyed', $.proxy(function(){ + this.destroy(); + }, this)); + + //attach document handler to close containers on click / escape + if(!$(document).data('editable-handlers-attached')) { + //close all on escape + $(document).on('keyup.editable', function (e) { + if (e.which === 27) { + $('.editable-open').editableContainer('hide'); + //todo: return focus on element + } + }); + + //close containers when click outside + //(mousedown could be better than click, it closes everything also on drag drop) + $(document).on('click.editable', function(e) { + var $target = $(e.target), i, + exclude_classes = ['.editable-container', + '.ui-datepicker-header', + '.datepicker', //in inline mode datepicker is rendered into body + '.modal-backdrop', + '.bootstrap-wysihtml5-insert-image-modal', + '.bootstrap-wysihtml5-insert-link-modal' + ]; + + //check if element is detached. It occurs when clicking in bootstrap datepicker + if (!$.contains(document.documentElement, e.target)) { + return; + } + + //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document + //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 + //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec + if($target.is(document)) { + return; + } + + //if click inside one of exclude classes --> no nothing + for(i=0; i<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(function() { + if(this.delayedHide) { + this.hide(this.delayedHide.reason); + this.delayedHide = false; + } else { + 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 will container do with content when closed: + remove(), detach() or just hide() - it depends on container. + + Detaching form itself before hide and re-insert before show is good solution, + but visually it looks ugly --> 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; + } + + //if form is saving value, schedule hide + if(this.$form.data('editableform').isSaving) { + this.delayedHide = {reason: reason}; + return; + } else { + this.delayedHide = false; + } + + this.$element.removeClass('editable-open'); + this.innerHide(); + + /** + Fired when container was hidden. It occurs on both save or cancel. + **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. + The workaround is to check `arguments.length` that is always `2` for x-editable. + + @event hidden + @param {object} event event object + @param {string} reason Reason caused hiding. Can be <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 of plugin, applied to input + this.hide('save'); + }, + + /** + Sets new option + + @method option(key, value) + @param {string} key + @param {mixed} value + **/ + option: function(key, value) { + this.options[key] = value; + if(key in this.containerOptions) { + this.containerOptions[key] = value; + this.setContainerOption(key, value); + } else { + this.formOptions[key] = value; + if(this.$form) { + this.$form.editableform('option', key, value); + } + } + }, + + setContainerOption: function(key, value) { + this.call('option', key, value); + }, + + /** + Destroys the container instance + @method destroy() + **/ + destroy: function() { + this.hide(); + this.innerDestroy(); + this.$element.off('destroyed'); + this.$element.removeData('editableContainer'); + }, + + /* to be overwritten in child classes */ + innerDestroy: function() { + + }, + + /* + Closes other containers except one related to passed element. + Other containers can be cancelled or submitted (depends on onblur option) + */ + closeOthers: function(element) { + $('.editable-open').each(function(i, el){ + //do nothing with passed element and it's children + if(el === element || $(el).find(element).length) { + return; + } + + //otherwise cancel or submit all open containers + var $el = $(el), + ec = $el.data('editableContainer'); + + if(!ec) { + return; + } + + if(ec.options.onblur === 'cancel') { + $el.data('editableContainer').hide('onblur'); + } else if(ec.options.onblur === 'submit') { + $el.data('editableContainer').tip().find('form').submit(); + } + }); + + }, + + /** + Activates input of visible container (e.g. set focus) + @method activate() + **/ + activate: function() { + if(this.tip && this.tip().is(':visible') && this.$form) { + this.$form.data('editableform').input.activate(); + } + } + + }; + + /** + jQuery method to initialize editableContainer. + + @method $().editableContainer(options) + @params {Object} options + @example + $('#edit').editableContainer({ + type: 'text', + url: '/post', + pk: 1, + value: 'hello' + }); + **/ + $.fn.editableContainer = function (option) { + var args = arguments; + return this.each(function () { + var $this = $(this), + dataKey = 'editableContainer', + data = $this.data(dataKey), + options = typeof option === 'object' && option, + Constructor = (options.mode === 'inline') ? Inline : Popup; + + if (!data) { + $this.data(dataKey, (data = new Constructor(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + //store constructors + $.fn.editableContainer.Popup = Popup; + $.fn.editableContainer.Inline = Inline; + + //defaults + $.fn.editableContainer.defaults = { + /** + Initial value of form input + + @property value + @type mixed + @default null + @private + **/ + value: null, + /** + Placement of container relative to element. Can be <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 only) + @property anim + @type string + @default false + **/ + anim: false, + + /** + Mode of editable, can be `popup` or `inline` + + @property mode + @type string + @default 'popup' + @since 1.4.0 + **/ + mode: 'popup' + }; + + /* + * workaround to have 'destroyed' event to destroy popover when element is destroyed + * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom + */ + jQuery.event.special.destroyed = { + remove: function(o) { + if (o.handler) { + o.handler(); + } + } + }; + +}(window.jQuery)); + +/** +* Editable Inline +* --------------------- +*/ +(function ($) { + "use strict"; + + //copy prototype from EditableContainer + //extend methods + $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { + containerName: 'editableform', + innerCss: '.editable-inline', + containerClass: 'editable-container editable-inline', //css class applied to container element + + initContainer: function(){ + //container is <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(); + } + + //check for transition support + if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { + this.options.highlight = false; + } + }; + + Editable.prototype = { + constructor: Editable, + init: function () { + var isValueByText = false, + doAutotext, finalize; + + //name + this.options.name = this.options.name || this.$element.attr('id'); + + //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select) + //also we set scope option to have access to element inside input specific callbacks (e. g. source as function) + this.options.scope = this.$element[0]; + this.input = $.fn.editableutils.createInput(this.options); + if(!this.input) { + return; + } + + //set value from settings or by element's text + if (this.options.value === undefined || this.options.value === null) { + this.value = this.input.html2value($.trim(this.$element.html())); + isValueByText = true; + } else { + /* + value can be string when received from 'data-value' attribute + for complext objects value can be set as json string in data-value attribute, + e.g. data-value="{city: 'Moscow', street: 'Lenina'}" + */ + this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); + if(typeof this.options.value === 'string') { + this.value = this.input.str2value(this.options.value); + } else { + this.value = this.options.value; + } + } + + //add 'editable' class to every editable element + this.$element.addClass('editable'); + + //attach handler activating editable. In disabled mode it just prevent default action (useful for links) + if(this.options.toggle !== 'manual') { + this.$element.addClass('editable-click'); + this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ + //prevent following link if editable enabled + if(!this.options.disabled) { + e.preventDefault(); + } + + //stop propagation not required because in document click handler it checks event target + //e.stopPropagation(); + + if(this.options.toggle === 'mouseenter') { + //for hover only show container + this.show(); + } else { + //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener + var closeAll = (this.options.toggle !== 'click'); + this.toggle(closeAll); + } + }, this)); + } else { + this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually + } + + //if display is function it's far more convinient to have autotext = always to render correctly on init + //see https://github.com/vitalets/x-editable-yii/issues/34 + if(typeof this.options.display === 'function') { + this.options.autotext = 'always'; + } + + //check conditions for autotext: + switch(this.options.autotext) { + case 'always': + doAutotext = true; + break; + case 'auto': + //if element text is empty and value is defined and value not generated by text --> run autotext + doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText; + break; + default: + doAutotext = false; + } + + //depending on autotext run render() or just finilize init + $.when(doAutotext ? this.render() : true).then($.proxy(function() { + if(this.options.disabled) { + this.disable(); + } else { + this.enable(); + } + /** + Fired when element was initialized by `$().editable()` method. + Please note that you should setup `init` handler **before** applying `editable`. + + @event init + @param {Object} event event object + @param {Object} editable editable instance (as here it cannot accessed via data('editable')) + @since 1.2.0 + @example + $('#username').on('init', function(e, editable) { + alert('initialized ' + editable.options.name); + }); + $('#username').editable(); + **/ + this.$element.triggerHandler('init', this); + }, this)); + }, + + /* + Initializes parent element for live editables + */ + initLive: function() { + //store selector + var selector = this.options.selector; + //modify options for child elements + this.options.selector = false; + this.options.autotext = 'never'; + //listen toggle events + this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ + var $target = $(e.target); + if(!$target.data('editable')) { + //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user) + //see https://github.com/vitalets/x-editable/issues/137 + if($target.hasClass(this.options.emptyclass)) { + $target.empty(); + } + $target.editable(this.options).trigger(e); + } + }, this)); + }, + + /* + Renders value into element's text. + Can call custom display method from options. + Can return deferred object. + @method render() + @param {mixed} response server response (if exist) to pass into display function + */ + render: function(response) { + //do not display anything + if(this.options.display === false) { + return; + } + + //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded + if(this.input.value2htmlFinal) { + return this.input.value2html(this.value, this.$element[0], this.options.display, response); + //if display method defined --> use it + } else if(typeof this.options.display === 'function') { + return this.options.display.call(this.$element[0], this.value, response); + //else use input's original value2html() method + } else { + return this.input.value2html(this.value, this.$element[0]); + } + }, + + /** + Enables editable + @method enable() + **/ + enable: function() { + this.options.disabled = false; + this.$element.removeClass('editable-disabled'); + this.handleEmpty(this.isEmpty); + if(this.options.toggle !== 'manual') { + if(this.$element.attr('tabindex') === '-1') { + this.$element.removeAttr('tabindex'); + } + } + }, + + /** + Disables editable + @method disable() + **/ + disable: function() { + this.options.disabled = true; + this.hide(); + this.$element.addClass('editable-disabled'); + this.handleEmpty(this.isEmpty); + //do not stop focus on this element + this.$element.attr('tabindex', -1); + }, + + /** + Toggles enabled / disabled state of editable element + @method toggleDisabled() + **/ + toggleDisabled: function() { + if(this.options.disabled) { + this.enable(); + } else { + this.disable(); + } + }, + + /** + Sets new option + + @method option(key, value) + @param {string|object} key option name or object with several options + @param {mixed} value option new value + @example + $('.editable').editable('option', 'pk', 2); + **/ + option: function(key, value) { + //set option(s) by object + if(key && typeof key === 'object') { + $.each(key, $.proxy(function(k, v){ + this.option($.trim(k), v); + }, this)); + return; + } + + //set option by string + this.options[key] = value; + + //disabled + if(key === 'disabled') { + return value ? this.disable() : this.enable(); + } + + //value + if(key === 'value') { + this.setValue(value); + } + + //transfer new option to container! + if(this.container) { + this.container.option(key, value); + } + + //pass option to input directly (as it points to the same in form) + if(this.input.option) { + this.input.option(key, value); + } + + }, + + /* + * set emptytext if element is empty + */ + handleEmpty: function (isEmpty) { + //do not handle empty if we do not display anything + if(this.options.display === false) { + return; + } + + /* + isEmpty may be set directly as param of method. + It is required when we enable/disable field and can't rely on content + as node content is text: "Empty" that is not empty %) + */ + if(isEmpty !== undefined) { + this.isEmpty = isEmpty; + } else { + //detect empty + if($.trim(this.$element.html()) === '') { + this.isEmpty = true; + } else if($.trim(this.$element.text()) !== '') { + this.isEmpty = false; + } else { + //e.g. '<img>' + this.isEmpty = !this.$element.height() || !this.$element.width(); + } + } + + //emptytext shown only for enabled + if(!this.options.disabled) { + if (this.isEmpty) { + this.$element.html(this.options.emptytext); + if(this.options.emptyclass) { + this.$element.addClass(this.options.emptyclass); + } + } else if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } + } else { + //below required if element disable property was changed + if(this.isEmpty) { + this.$element.empty(); + if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } + } + } + }, + + /** + Shows container with form + @method show() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + show: function (closeAll) { + if(this.options.disabled) { + return; + } + + //init editableContainer: popover, tooltip, inline, etc.. + if(!this.container) { + var containerOptions = $.extend({}, this.options, { + value: this.value, + input: this.input //pass input to form (as it is already created) + }); + this.$element.editableContainer(containerOptions); + //listen `save` event + this.$element.on("save.internal", $.proxy(this.save, this)); + this.container = this.$element.data('editableContainer'); + } else if(this.container.tip().is(':visible')) { + return; + } + + //show container + this.container.show(closeAll); + }, + + /** + Hides container with form + @method hide() + **/ + hide: function () { + if(this.container) { + this.container.hide(); + } + }, + + /** + Toggles container visibility (show / hide) + @method toggle() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + toggle: function(closeAll) { + if(this.container && this.container.tip().is(':visible')) { + this.hide(); + } else { + this.show(closeAll); + } + }, + + /* + * called when form was submitted + */ + save: function(e, params) { + //mark element with unsaved class if needed + if(this.options.unsavedclass) { + /* + Add unsaved css to element if: + - url is not user's function + - value was not sent to server + - params.response === undefined, that means data was not sent + - value changed + */ + var sent = false; + sent = sent || typeof this.options.url === 'function'; + sent = sent || this.options.display === false; + sent = sent || params.response !== undefined; + sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); + + if(sent) { + this.$element.removeClass(this.options.unsavedclass); + } else { + this.$element.addClass(this.options.unsavedclass); + } + } + + //highlight when saving + if(this.options.highlight) { + var $e = this.$element, + $bgColor = $e.css('background-color'); + + $e.css('background-color', this.options.highlight); + setTimeout(function(){ + $e.css('background-color', $bgColor); + $e.addClass('editable-bg-transition'); + setTimeout(function(){ + $e.removeClass('editable-bg-transition'); + }, 1700); + }, 0); + } + + //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(); + } + + this.input.destroy(); + + if(this.options.toggle !== 'manual') { + this.$element.removeClass('editable-click'); + this.$element.off(this.options.toggle + '.editable'); + } + + this.$element.off("save.internal"); + + this.$element.removeClass('editable editable-open editable-disabled'); + this.$element.removeData('editable'); + } + }; + + /* EDITABLE PLUGIN DEFINITION + * ======================= */ + + /** + jQuery method to initialize editable element. + + @method $().editable(options) + @params {Object} options + @example + $('#username').editable({ + type: 'text', + url: '/post', + pk: 1 + }); + **/ + $.fn.editable = function (option) { + //special API methods returning non-jquery object + var result = {}, args = arguments, datakey = 'editable'; + switch (option) { + /** + Runs client-side validation for all matched editables + + @method validate() + @returns {Object} validation errors map + @example + $('#username, #fullname').editable('validate'); + // possible result: + { + username: "username is required", + fullname: "fullname should be minimum 3 letters length" + } + **/ + case 'validate': + this.each(function () { + var $this = $(this), data = $this.data(datakey), error; + if (data && (error = data.validate())) { + result[data.options.name] = error; + } + }); + return result; + + /** + Returns current values of editable elements. + Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. + If value of some editable is `null` or `undefined` it is excluded from result object. + When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. + + @method getValue() + @param {bool} isSingle whether to return just value of single element + @returns {Object} object of element names and values + @example + $('#username, #fullname').editable('getValue'); + //result: + { + username: "superuser", + fullname: "John" + } + //isSingle = true + $('#username').editable('getValue', true); + //result "superuser" + **/ + case 'getValue': + if(arguments.length === 2 && arguments[1] === true) { //isSingle = true + result = this.eq(0).data(datakey).value; + } else { + this.each(function () { + var $this = $(this), data = $this.data(datakey); + if (data && data.value !== undefined && data.value !== null) { + result[data.options.name] = data.input.value2submit(data.value); + } + }); + } + return result; + + /** + This method collects values from several editable elements and submit them all to server. + Internally it runs client-side validation for all fields and submits only in case of success. + See <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, + /** + Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. + + @property highlight + @type string|boolean + @since 1.4.5 + @default #FFFF80 + **/ + highlight: '#FFFF80' + }; + +}(window.jQuery)); + +/** +AbstractInput - base class for all editable inputs. +It defines interface to be implemented by any input type. +To create your own input you can inherit from this class. + +@class abstractinput +**/ +(function ($) { + "use strict"; + + //types + $.fn.editabletypes = {}; + + var AbstractInput = function () { }; + + AbstractInput.prototype = { + /** + Initializes input + + @method init() + **/ + init: function(type, options, defaults) { + this.type = type; + this.options = $.extend({}, defaults, options); + }, + + /* + this method called before render to init $tpl that is inserted in DOM + */ + prerender: function() { + this.$tpl = $(this.options.tpl); //whole tpl as jquery object + this.$input = this.$tpl; //control itself, can be changed in render method + this.$clear = null; //clear button + this.error = null; //error message, if input cannot be rendered + }, + + /** + Renders input from tpl. Can return jQuery deferred object. + Can be overwritten in child objects + + @method render() + **/ + render: function() { + + }, + + /** + Sets element's html by value. + + @method value2html(value, element) + @param {mixed} value + @param {DOMElement} element + **/ + value2html: function(value, element) { + $(element).text($.trim(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() { + + }, + + /** + Additional actions when destroying element + **/ + destroy: function() { + }, + + // -------- helper functions -------- + setClass: function() { + if(this.options.inputclass) { + this.$input.addClass(this.options.inputclass); + } + }, + + setAttr: function(attr) { + if (this.options[attr] !== undefined && this.options[attr] !== null) { + this.$input.attr(attr, this.options[attr]); + } + }, + + option: function(key, value) { + this.options[key] = value; + } + + }; + + AbstractInput.defaults = { + /** + HTML template of input. Normally you should not change it. + + @property tpl + @type string + @default '' + **/ + tpl: '', + /** + CSS class automatically applied to input + + @property inputclass + @type string + @default 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) { + //run source if it function + var source; + if ($.isFunction(this.options.source)) { + source = this.options.source.call(this.options.scope); + this.sourceData = null; + //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed + } else { + source = this.options.source; + } + + //if allready loaded just call success + if(this.options.sourceCache && $.isArray(this.sourceData)) { + success.call(this); + return; + } + + //try parse json in single quotes (for double quotes jquery does automatically) + try { + source = $.fn.editableutils.tryParseJson(source, false); + } catch (e) { + error.call(this); + return; + } + + //loading from url + if (typeof source === 'string') { + //try to get sourceData from cache + if(this.options.sourceCache) { + var cacheID = source, + cache; + + if (!$(document).data(cacheID)) { + $(document).data(cacheID, {}); + } + cache = $(document).data(cacheID); + + //check for cached data + if (cache.loading === false && cache.sourceData) { //take source from cache + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + return; + } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later + cache.callbacks.push($.proxy(function () { + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + }, this)); + + //also collecting error callbacks + cache.err_callbacks.push($.proxy(error, this)); + return; + } else { //no cache yet, activate it + cache.loading = true; + cache.callbacks = []; + cache.err_callbacks = []; + } + } + + //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 select2 docs](http://ivaynberg.github.com/select2) for detailed description and options. +Compatible **select2 version is 3.4.1**! +You should manually download and include select2 distributive: + + <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> + <script src="select2/select2.js"></script> + +To 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 `autotext` feature does not work for select2 with `ajax` remote source. +You need initially put both `data-value` and element's text youself: + + <a href="#" data-type="select2" data-value="1">Text1</a> + + +@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-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 || {}; + + this.sourceData = null; + + //placeholder + if(options.placeholder) { + options.select2.placeholder = options.placeholder; + } + + //if not `tags` mode, use source + if(!options.select2.tags && options.source) { + var source = options.source; + //if source is function, call it (once!) + if ($.isFunction(options.source)) { + source = options.source.call(options.scope); + } + + if (typeof source === 'string') { + options.select2.ajax = options.select2.ajax || {}; + //some default ajax params + if(!options.select2.ajax.data) { + options.select2.ajax.data = function(term) {return { query:term };}; + } + if(!options.select2.ajax.results) { + options.select2.ajax.results = function(data) { return {results:data };}; + } + options.select2.ajax.url = source; + } else { + //check format and convert x-editable format to select2 format (if needed) + this.sourceData = this.convertSource(source); + options.select2.data = this.sourceData; + } + } + + //overriding objects in config (as by default jQuery extend() is not recursive) + this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2); + + //detect whether it is multi-valued + this.isMultiple = this.options.select2.tags || this.options.select2.multiple; + this.isRemote = ('ajax' in this.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 to populate listData + if(this.isRemote) { + //listen to loaded event to populate data + this.$input.on('select2-loaded', $.proxy(function(e) { + this.sourceData = e.items.results; + }, this)); + } + + //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.options.select2.tags) { //in tags mode just assign value + data = value; + } else if(this.sourceData) { + data = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + } else { + //can not get list of possible values (e.g. autotext for select2 with ajax source) + } + + //data may be array (when multiple values allowed) + 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) { + //for remote source .val() is not working, need to look in sourceData + if(this.isRemote) { + //todo: check value for array + var item, items; + //if sourceData loaded, use it to get text for display + if(this.sourceData) { + items = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + if(items.length) { + item = items[0]; + } + } + //if item not found by sourceData, use element text (e.g. for the first show) + if(!item) { + item = {id: value, text: $(this.options.scope).text()}; + } + //select2('data', ...) allows to set both id and text --> usefull for initial show when items are not loaded + this.$input.select2('data', item).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + } else { + 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(); + } + }); + }, + + /* + Converts source from x-editable format: {value: 1, text: "1"} to + select2 format: {id: 1, text: "1"} + */ + convertSource: function(source) { + if($.isArray(source) && source.length && source[0].value !== undefined) { + for(var i = 0; i<source.length; i++) { + if(source[i].value !== undefined) { + source[i].id = source[i].value; + delete source[i].value; + } + } + } + return source; + } + + }); + + 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.4 +* 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 +* +* Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight +* In combodate: +* 12:00 pm --> 12:00 (24-h format, midday) +* 12:00 am --> 00:00 (24-h format, midnight, start of day) +* +* Differs from momentjs parse rules: +* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change) +* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change) +* +* +* 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) { + //see https://github.com/timrwood/momentjs.com/pull/36 + name = moment().date(1).month(i).format('MMMM'); + } else if(shortNames) { + name = moment().date(1).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, + min = h12 ? 1 : 0, + max = h12 ? 12 : 23; + + for(i=min; 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 from combos. + 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 12h --> 24h + if(this.$ampm) { + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour === 12) { + values.hour = this.$ampm.val() === 'am' ? 0 : 12; + } else { + values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; + } + } + + 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) { + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour >= 12) { + values.ampm = 'pm'; + if(values.hour > 12) { + values.hour -= 12; + } + } else { + values.ampm = 'am'; + if(values.hour === 0) { + values.hour = 12; + } + } + } + + $.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.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._process_options(options); + + this.element = $(element); + 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.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.o.rtl){ + this.picker.addClass('datepicker-rtl'); + this.picker.find('.prev i, .next i') + .toggleClass('icon-arrow-left icon-arrow-right'); + } + + + this.viewMode = this.o.startView; + + if (this.o.calendarWeeks) + this.picker.find('tfoot th.today') + .attr('colspan', function(i, val){ + return parseInt(val) + 1; + }); + + this._allow_update = false; + + this.setStartDate(this.o.startDate); + this.setEndDate(this.o.endDate); + this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled); + + this.fillDow(); + this.fillMonths(); + + this._allow_update = true; + + this.update(); + this.showMode(); + + if(this.isInline) { + this.show(); + } + }; + + Datepicker.prototype = { + constructor: Datepicker, + + _process_options: function(opts){ + // Store raw options for reference + this._o = $.extend({}, this._o, opts); + // Processed options + var o = this.o = $.extend({}, this._o); + + // Check if "de-DE" style date is available, if not language should + // fallback to 2 letter code eg "de" + var lang = o.language; + if (!dates[lang]) { + lang = lang.split('-')[0]; + if (!dates[lang]) + lang = defaults.language; + } + o.language = lang; + + switch(o.startView){ + case 2: + case 'decade': + o.startView = 2; + break; + case 1: + case 'year': + o.startView = 1; + break; + default: + o.startView = 0; + } + + switch (o.minViewMode) { + case 1: + case 'months': + o.minViewMode = 1; + break; + case 2: + case 'years': + o.minViewMode = 2; + break; + default: + o.minViewMode = 0; + } + + o.startView = Math.max(o.startView, o.minViewMode); + + o.weekStart %= 7; + o.weekEnd = ((o.weekStart + 6) % 7); + + var format = DPGlobal.parseFormat(o.format) + if (o.startDate !== -Infinity) { + o.startDate = DPGlobal.parseDate(o.startDate, format, o.language); + } + if (o.endDate !== Infinity) { + o.endDate = DPGlobal.parseDate(o.endDate, format, o.language); + } + + o.daysOfWeekDisabled = o.daysOfWeekDisabled||[]; + if (!$.isArray(o.daysOfWeekDisabled)) + o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/); + o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function (d) { + return parseInt(d, 10); + }); + }, + _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 (!( + this.element.is(e.target) || + this.element.find(e.target).size() || + this.picker.is(e.target) || + this.picker.find(e.target).size() + )) { + 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); + }, + _trigger: function(event, altdate){ + var date = altdate || this.date, + local_date = new Date(date.getTime() + (date.getTimezoneOffset()*60000)); + + this.element.trigger({ + type: event, + date: local_date, + format: $.proxy(function(altformat){ + var format = altformat || this.o.format; + return DPGlobal.formatDate(date, format, this.o.language); + }, this) + }); + }, + + 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._trigger('show'); + }, + + hide: function(e){ + if(this.isInline) return; + if (!this.picker.is(':visible')) return; + this.picker.hide().detach(); + this._detachSecondaryEvents(); + this.viewMode = this.o.startView; + this.showMode(); + + if ( + this.o.forceParse && + ( + this.isInput && this.element.val() || + this.hasInput && this.element.find('input').val() + ) + ) + this.setValue(); + this._trigger('hide'); + }, + + 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); + } + } else { + this.element.val(formatted); + } + }, + + getFormattedDate: function(format) { + if (format === undefined) + format = this.o.format; + return DPGlobal.formatDate(this.date, format, this.o.language); + }, + + setStartDate: function(startDate){ + this._process_options({startDate: startDate}); + this.update(); + this.updateNavArrows(); + }, + + setEndDate: function(endDate){ + this._process_options({endDate: endDate}); + this.update(); + this.updateNavArrows(); + }, + + setDaysOfWeekDisabled: function(daysOfWeekDisabled){ + this._process_options({daysOfWeekDisabled: daysOfWeekDisabled}); + 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(); + delete this.element.data().date; + } + + this.date = DPGlobal.parseDate(date, this.o.format, this.o.language); + + if(fromArgs) this.setValue(); + + if (this.date < this.o.startDate) { + this.viewDate = new Date(this.o.startDate); + } else if (this.date > this.o.endDate) { + this.viewDate = new Date(this.o.endDate); + } else { + this.viewDate = new Date(this.date); + } + this.fill(); + }, + + fillDow: function(){ + var dowCnt = this.o.weekStart, + html = '<tr>'; + if(this.o.calendarWeeks){ + var cell = '<th class="cw"> </th>'; + html += cell; + this.picker.find('.datepicker-days thead tr:first-child').prepend(cell); + } + while (dowCnt < this.o.weekStart + 7) { + html += '<th class="dow">'+dates[this.o.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.o.language].monthsShort[i++]+'</span>'; + } + this.picker.find('.datepicker-months td').html(html); + }, + + setRange: function(range){ + if (!range || !range.length) + delete this.range; + else + this.range = $.map(range, function(d){ return d.valueOf(); }); + this.fill(); + }, + + getClassNames: function(date){ + var cls = [], + year = this.viewDate.getUTCFullYear(), + month = this.viewDate.getUTCMonth(), + currentDate = this.date.valueOf(), + today = new Date(); + if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) { + cls.push('old'); + } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) { + cls.push('new'); + } + // Compare internal UTC date with local today, not UTC today + if (this.o.todayHighlight && + date.getUTCFullYear() == today.getFullYear() && + date.getUTCMonth() == today.getMonth() && + date.getUTCDate() == today.getDate()) { + cls.push('today'); + } + if (currentDate && date.valueOf() == currentDate) { + cls.push('active'); + } + if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate || + $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) { + cls.push('disabled'); + } + if (this.range){ + if (date > this.range[0] && date < this.range[this.range.length-1]){ + cls.push('range'); + } + if ($.inArray(date.valueOf(), this.range) != -1){ + cls.push('selected'); + } + } + return cls; + }, + + fill: function() { + var d = new Date(this.viewDate), + year = d.getUTCFullYear(), + month = d.getUTCMonth(), + startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, + startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, + endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, + endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, + currentDate = this.date && this.date.valueOf(), + tooltip; + this.picker.find('.datepicker-days thead th.datepicker-switch') + .text(dates[this.o.language].months[month]+' '+year); + this.picker.find('tfoot th.today') + .text(dates[this.o.language].today) + .toggle(this.o.todayBtn !== false); + this.picker.find('tfoot th.clear') + .text(dates[this.o.language].clear) + .toggle(this.o.clearBtn !== 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.o.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.o.weekStart) { + html.push('<tr>'); + if(this.o.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.o.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 = this.getClassNames(prevMonth); + clsName.push('day'); + + var before = this.o.beforeShowDay(prevMonth); + if (before === undefined) + before = {}; + else if (typeof(before) === 'boolean') + before = {enabled: before}; + else if (typeof(before) === 'string') + before = {classes: before}; + if (before.enabled === false) + clsName.push('disabled'); + if (before.classes) + clsName = clsName.concat(before.classes.split(/\s+/)); + if (before.tooltip) + tooltip = before.tooltip; + + clsName = $.unique(clsName); + html.push('<td class="'+clsName.join(' ')+'"' + (tooltip ? ' title="'+tooltip+'"' : '') + '>'+prevMonth.getUTCDate() + '</td>'); + if (prevMonth.getUTCDay() == this.o.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 ? ' old' : i == 10 ? ' new' : '')+(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.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.startDate.getUTCMonth()) { + this.picker.find('.prev').css({visibility: 'hidden'}); + } else { + this.picker.find('.prev').css({visibility: 'visible'}); + } + if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()) { + this.picker.find('.next').css({visibility: 'hidden'}); + } else { + this.picker.find('.next').css({visibility: 'visible'}); + } + break; + case 1: + case 2: + if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear()) { + this.picker.find('.prev').css({visibility: 'hidden'}); + } else { + this.picker.find('.prev').css({visibility: 'visible'}); + } + if (this.o.endDate !== Infinity && year >= this.o.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 'datepicker-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.o.todayBtn == 'linked' ? null : 'view'; + this._setDate(date, which); + break; + case 'clear': + var element; + if (this.isInput) + element = this.element; + else if (this.component) + element = this.element.find('input'); + if (element) + element.val("").change(); + this._trigger('changeDate'); + this.update(); + if (this.o.autoclose) + this.hide(); + 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._trigger('changeMonth', this.viewDate); + if (this.o.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._trigger('changeYear', this.viewDate); + if (this.o.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 = new Date(date); + if (!which || which == 'view') + this.viewDate = new Date(date); + this.fill(); + this.setValue(); + this._trigger('changeDate'); + var element; + if (this.isInput) { + element = this.element; + } else if (this.component){ + element = this.element.find('input'); + } + if (element) { + element.change(); + if (this.o.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.o.startDate && date <= this.o.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.o.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.o.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._trigger('changeDate'); + 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.o.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(); + } + }; + + var DateRangePicker = function(element, options){ + this.element = $(element); + this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; }); + delete options.inputs; + + $(this.inputs) + .datepicker(options) + .bind('changeDate', $.proxy(this.dateUpdated, this)); + + this.pickers = $.map(this.inputs, function(i){ return $(i).data('datepicker'); }); + this.updateDates(); + }; + DateRangePicker.prototype = { + updateDates: function(){ + this.dates = $.map(this.pickers, function(i){ return i.date; }); + this.updateRanges(); + }, + updateRanges: function(){ + var range = $.map(this.dates, function(d){ return d.valueOf(); }); + $.each(this.pickers, function(i, p){ + p.setRange(range); + }); + }, + dateUpdated: function(e){ + var dp = $(e.target).data('datepicker'), + new_date = dp.getUTCDate(), + i = $.inArray(e.target, this.inputs), + l = this.inputs.length; + if (i == -1) return; + + if (new_date < this.dates[i]){ + // Date being moved earlier/left + while (i>=0 && new_date < this.dates[i]){ + this.pickers[i--].setUTCDate(new_date); + } + } + else if (new_date > this.dates[i]){ + // Date being moved later/right + while (i<l && new_date > this.dates[i]){ + this.pickers[i++].setUTCDate(new_date); + } + } + this.updateDates(); + }, + remove: function(){ + $.map(this.pickers, function(p){ p.remove(); }); + delete this.element.data().datepicker; + } + }; + + function opts_from_el(el, prefix){ + // Derive options from element data-attrs + var data = $(el).data(), + out = {}, inkey, + replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'), + prefix = new RegExp('^' + prefix.toLowerCase()); + for (var key in data) + if (prefix.test(key)){ + inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); }); + out[inkey] = data[key]; + } + return out; + } + + function opts_from_locale(lang){ + // Derive options from locale plugins + var out = {}; + // Check if "de-DE" style date is available, if not language should + // fallback to 2 letter code eg "de" + if (!dates[lang]) { + lang = lang.split('-')[0] + if (!dates[lang]) + return; + } + var d = dates[lang]; + $.each(locale_opts, function(i,k){ + if (k in d) + out[k] = d[k]; + }); + return out; + } + + var old = $.fn.datepicker; + var datepicker = $.fn.datepicker = function ( option ) { + var args = Array.apply(null, arguments); + args.shift(); + var internal_return, + this_return; + this.each(function () { + var $this = $(this), + data = $this.data('datepicker'), + options = typeof option == 'object' && option; + if (!data) { + var elopts = opts_from_el(this, 'date'), + // Preliminary otions + xopts = $.extend({}, defaults, elopts, options), + locopts = opts_from_locale(xopts.language), + // Options priority: js args, data-attrs, locales, defaults + opts = $.extend({}, defaults, locopts, elopts, options); + if ($this.is('.input-daterange') || opts.inputs){ + var ropts = { + inputs: opts.inputs || $this.find('input').toArray() + }; + $this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts)))); + } + else{ + $this.data('datepicker', (data = new Datepicker(this, opts))); + } + } + if (typeof option == 'string' && typeof data[option] == 'function') { + internal_return = data[option].apply(data, args); + if (internal_return !== undefined) + return false; + } + }); + if (internal_return !== undefined) + return internal_return; + else + return this; + }; + + var defaults = $.fn.datepicker.defaults = { + autoclose: false, + beforeShowDay: $.noop, + calendarWeeks: false, + clearBtn: false, + daysOfWeekDisabled: [], + endDate: Infinity, + forceParse: true, + format: 'mm/dd/yyyy', + keyboardNavigation: true, + language: 'en', + minViewMode: 0, + rtl: false, + startDate: -Infinity, + startView: 0, + todayBtn: false, + todayHighlight: false, + weekStart: 0 + }; + var locale_opts = $.fn.datepicker.locale_opts = [ + 'format', + 'rtl', + 'weekStart' + ]; + $.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", + clear: "Clear" + } + }; + + 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 (typeof format === 'string') + format = DPGlobal.parseFormat(format); + 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){ + if (typeof format === 'string') + format = DPGlobal.parseFormat(format); + 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="datepicker-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><tr><th colspan="7" class="clear"></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; + + + /* DATEPICKER NO CONFLICT + * =================== */ + + $.fn.datepicker.noConflict = function(){ + $.fn.datepicker = old; + return this; + }; + + + /* DATEPICKER DATA-API + * ================== */ + + $(document).on( + 'focus.datepicker.data-api click.datepicker.data-api', + '[data-provide="datepicker"]', + function(e){ + var $this = $(this); + if ($this.data('datepicker')) return; + e.preventDefault(); + // component click requires us to explicitly show it + datepicker.call($this, 'show'); + } + ); + $(function(){ + //$('[data-provide="datepicker-inline"]').datepicker(); + //vit: changed to support noConflict() + datepicker.call($('[data-provide="datepicker-inline"]')); + }); + +}( 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"; + + //store bootstrap-datepicker as bdateicker to exclude conflict with jQuery UI one + $.fn.bdatepicker = $.fn.datepicker.noConflict(); + if(!$.fn.datepicker) { //if there were no other datepickers, keep also original name + $.fn.datepicker = $.fn.bdatepicker; + } + + 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.bdatepicker.DPGlobal; + + //store parsed formats + this.parsedFormat = this.dpg.parseFormat(this.options.format); + this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat); + }, + + render: function () { + this.$input.bdatepicker(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 this.parseDate(html, this.parsedViewFormat); + }, + + value2str: function(value) { + return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : ''; + }, + + str2value: function(str) { + return this.parseDate(str, this.parsedFormat); + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + this.$input.bdatepicker('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); + }); + */ + }, + + /* + For incorrect date bootstrap-datepicker returns current date that is not suitable + for datefield. + This function returns null for incorrect date. + */ + parseDate: function(str, format) { + var date = null, formattedBack; + if(str) { + date = this.dpg.parseDate(str, format, this.options.datepicker.language); + if(typeof str === 'string') { + formattedBack = this.dpg.formatDate(date, format, this.options.datepicker.language); + if(str !== formattedBack) { + date = null; + } + } + } + return date; + } + + }); + + 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'); + + //bootstrap-datepicker is set `bdateicker` to exclude conflict with jQuery UI one. (in date.js) + this.$tpl.bdatepicker(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.bdatepicker('update'); + }, this)); + + }, + + value2input: function(value) { + this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''); + this.$tpl.bdatepicker('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-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); + }, + + render: function () { + this.$input.datetimepicker(this.options.datetimepicker); + + //adjust container position when viewMode changes + //see https://github.com/smalot/bootstrap-datetimepicker/pull/80 + this.$input.on('changeMode', function(e) { + var f = $(this).closest('form').parent(); + //timeout here, otherwise container changes position before form has new size + setTimeout(function(){ + f.triggerHandler('resize'); + }, 0); + }); + + //"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 = this.parseDate(html, this.parsedViewFormat); + 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 = this.parseDate(str, this.parsedFormat); + 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; + }, + + /* + For incorrect date bootstrap-datetimepicker returns current date that is not suitable + for datetimefield. + This function returns null for incorrect date. + */ + parseDate: function(str, format) { + var date = null, formattedBack; + if(str) { + date = this.dpg.parseDate(str, format, this.options.datetimepicker.language, this.options.formatType); + if(typeof str === 'string') { + formattedBack = this.dpg.formatDate(date, format, this.options.datetimepicker.language, this.options.formatType); + if(str !== formattedBack) { + date = null; + } + } + } + return date; + } + + }); + + 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..ab4a62d --- /dev/null +++ b/dist/bootstrap-editable/js/bootstrap-editable.min.js @@ -0,0 +1,5 @@ +/*! X-editable - v1.4.5 +* 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.isSaving=!1,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}var i=this.input.value2submit(r);this.isSaving=!0,e.when(this.save(i)).done(e.proxy(function(e){this.isSaving=!1;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,submitValue:i,response:e})},this)).fail(e.proxy(function(e){this.isSaving=!1;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){this.options.pk=e.fn.editableutils.tryParseJson(this.options.pk,!0);var n=typeof this.options.pk=="function"?this.options.pk.call(this.options.scope):this.options.pk,r=!!(typeof this.options.url=="function"||this.options.url&&(this.options.send==="always"||this.options.send==="auto"&&n!==null&&n!==undefined)),i;if(r)return this.showLoading(),i={name:this.options.name||"",value:t,pk:n},typeof this.options.params=="function"?i=this.options.params.call(this.options.scope,i):(this.options.params=e.fn.editableutils.tryParseJson(this.options.params,!0),e.extend(i,this.options.params)),typeof this.options.url=="function"?this.options.url.call(this.options.scope,i):e.ajax(e.extend({url:this.options.url,data:i,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)},supportsTransitions:function(){var e=document.body||document.documentElement,t=e.style,n="transition",r=["Moz","Webkit","Khtml","O","ms"];if(typeof t[n]=="string")return!0;n=n.charAt(0).toUpperCase()+n.substr(1);for(var i=0;i<r.length;i++)if(typeof t[r[i]+n]=="string")return!0;return!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.delayedHide=!1,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(function(){this.delayedHide?(this.hide(this.delayedHide.reason),this.delayedHide=!1):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;if(this.$form.data("editableform").isSaving){this.delayedHide={reason:e};return}this.delayedHide=!1,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(),this.options.highlight&&!e.fn.editableutils.supportsTransitions()&&(this.options.highlight=!1)};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){this.options.disabled||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),typeof this.options.display=="function"&&(this.options.autotext="always");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;t!==undefined?this.isEmpty=t:e.trim(this.$element.html())===""?this.isEmpty=!0:e.trim(this.$element.text())!==""?this.isEmpty=!1:this.isEmpty=!this.$element.height()||!this.$element.width(),this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.html(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)}if(this.options.highlight){var r=this.$element,i=r.css("background-color");r.css("background-color",this.options.highlight),setTimeout(function(){r.css("background-color",i),r.addClass("editable-bg-transition"),setTimeout(function(){r.removeClass("editable-bg-transition")},1700)},0)}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.input.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 arguments.length===2&&arguments[1]===!0?r=this.eq(0).data(s).value: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,highlight:"#FFFF80"}}(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(e.trim(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(){},destroy: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){var r;e.isFunction(this.options.source)?(r=this.options.source.call(this.options.scope),this.sourceData=null):r=this.options.source;if(this.options.sourceCache&&e.isArray(this.sourceData)){t.call(this);return}try{r=e.fn.editableutils.tryParseJson(r,!1)}catch(i){n.call(this);return}if(typeof r=="string"){if(this.options.sourceCache){var s=r,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:r,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(r),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||{},this.sourceData=null,n.placeholder&&(n.select2.placeholder=n.placeholder);if(!n.select2.tags&&n.source){var r=n.source;e.isFunction(n.source)&&(r=n.source.call(n.scope)),typeof r=="string"?(n.select2.ajax=n.select2.ajax||{},n.select2.ajax.data||(n.select2.ajax.data=function(e){return{query:e}}),n.select2.ajax.results||(n.select2.ajax.results=function(e){return{results:e}}),n.select2.ajax.url=r):(this.sourceData=this.convertSource(r),n.select2.data=this.sourceData)}this.options.select2=e.extend({},t.defaults.select2,n.select2),this.isMultiple=this.options.select2.tags||this.options.select2.multiple,this.isRemote="ajax"in this.options.select2};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.$input.select2(this.options.select2),this.isRemote&&this.$input.on("select2-loaded",e.proxy(function(e){this.sourceData=e.items.results},this)),this.isMultiple&&this.$input.on("change",function(){e(this).closest("form").parent().triggerHandler("resize")})},value2html:function(t,n){var r="",i;this.options.select2.tags?i=t:this.sourceData&&(i=e.fn.editableutils.itemsByValue(t,this.sourceData,"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(t){if(this.isRemote){var n,r;this.sourceData&&(r=e.fn.editableutils.itemsByValue(t,this.sourceData,"id"),r.length&&(n=r[0])),n||(n={id:t,text:e(this.options.scope).text()}),this.$input.select2("data",n).trigger("change",!0)}else this.$input.val(t).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()})},convertSource:function(t){if(e.isArray(t)&&t.length&&t[0].value!==undefined)for(var n=0;n<t.length;n++)t[n].value!==undefined&&(t[n].id=t[n].value,delete t[n].value);return t}}),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().date(1).month(n).format("MMMM"):i?t=moment().date(1).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?1:0,u=r?12:23;for(n=o;n<=u;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===12?r.hour=this.$ampm.val()==="am"?0:12:r.hour=this.$ampm.val()==="am"?r.hour:r.hour+12),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.ampm="pm",i.hour>12&&(i.hour-=12)):(i.ampm="am",i.hour===0&&(i.hour=12))),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){function t(){return new Date(Date.UTC.apply(Date,arguments))}function n(){var e=new Date;return t(e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate())}function s(t,n){var r=e(t).data(),i={},s,o=new RegExp("^"+n.toLowerCase()+"([A-Z])"),n=new RegExp("^"+n.toLowerCase());for(var u in r)n.test(u)&&(s=u.replace(o,function(e,t){return t.toLowerCase()}),i[s]=r[u]);return i}function o(t){var n={};if(!c[t]){t=t.split("-")[0];if(!c[t])return}var r=c[t];return e.each(l,function(e,t){t in r&&(n[t]=r[t])}),n}var r=function(t,n){var r=this;this._process_options(n),this.element=e(t),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.picker=e(h.template),this._buildEvents(),this._attachEvents(),this.isInline?this.picker.addClass("datepicker-inline").appendTo(this.element):this.picker.addClass("datepicker-dropdown dropdown-menu"),this.o.rtl&&(this.picker.addClass("datepicker-rtl"),this.picker.find(".prev i, .next i").toggleClass("icon-arrow-left icon-arrow-right")),this.viewMode=this.o.startView,this.o.calendarWeeks&&this.picker.find("tfoot th.today").attr("colspan",function(e,t){return parseInt(t)+1}),this._allow_update=!1,this.setStartDate(this.o.startDate),this.setEndDate(this.o.endDate),this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled),this.fillDow(),this.fillMonths(),this._allow_update=!0,this.update(),this.showMode(),this.isInline&&this.show()};r.prototype={constructor:r,_process_options:function(t){this._o=e.extend({},this._o,t);var n=this.o=e.extend({},this._o),r=n.language;c[r]||(r=r.split("-")[0],c[r]||(r=f.language)),n.language=r;switch(n.startView){case 2:case"decade":n.startView=2;break;case 1:case"year":n.startView=1;break;default:n.startView=0}switch(n.minViewMode){case 1:case"months":n.minViewMode=1;break;case 2:case"years":n.minViewMode=2;break;default:n.minViewMode=0}n.startView=Math.max(n.startView,n.minViewMode),n.weekStart%=7,n.weekEnd=(n.weekStart+6)%7;var i=h.parseFormat(n.format);n.startDate!==-Infinity&&(n.startDate=h.parseDate(n.startDate,i,n.language)),n.endDate!==Infinity&&(n.endDate=h.parseDate(n.endDate,i,n.language)),n.daysOfWeekDisabled=n.daysOfWeekDisabled||[],e.isArray(n.daysOfWeekDisabled)||(n.daysOfWeekDisabled=n.daysOfWeekDisabled.split(/[,\s]*/)),n.daysOfWeekDisabled=e.map(n.daysOfWeekDisabled,function(e){return parseInt(e,10)})},_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(e){this.element.is(e.target)||this.element.find(e.target).size()||this.picker.is(e.target)||this.picker.find(e.target).size()||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)},_trigger:function(t,n){var r=n||this.date,i=new Date(r.getTime()+r.getTimezoneOffset()*6e4);this.element.trigger({type:t,date:i,format:e.proxy(function(e){var t=e||this.o.format;return h.formatDate(r,t,this.o.language)},this)})},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._trigger("show")},hide:function(e){if(this.isInline)return;if(!this.picker.is(":visible"))return;this.picker.hide().detach(),this._detachSecondaryEvents(),this.viewMode=this.o.startView,this.showMode(),this.o.forceParse&&(this.isInput&&this.element.val()||this.hasInput&&this.element.find("input").val())&&this.setValue(),this._trigger("hide")},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)},getFormattedDate:function(e){return e===undefined&&(e=this.o.format),h.formatDate(this.date,e,this.o.language)},setStartDate:function(e){this._process_options({startDate:e}),this.update(),this.updateNavArrows()},setEndDate:function(e){this._process_options({endDate:e}),this.update(),this.updateNavArrows()},setDaysOfWeekDisabled:function(e){this._process_options({daysOfWeekDisabled:e}),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(),delete this.element.data().date),this.date=h.parseDate(e,this.o.format,this.o.language),t&&this.setValue(),this.date<this.o.startDate?this.viewDate=new Date(this.o.startDate):this.date>this.o.endDate?this.viewDate=new Date(this.o.endDate):this.viewDate=new Date(this.date),this.fill()},fillDow:function(){var e=this.o.weekStart,t="<tr>";if(this.o.calendarWeeks){var n='<th class="cw"> </th>';t+=n,this.picker.find(".datepicker-days thead tr:first-child").prepend(n)}while(e<this.o.weekStart+7)t+='<th class="dow">'+c[this.o.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">'+c[this.o.language].monthsShort[t++]+"</span>";this.picker.find(".datepicker-months td").html(e)},setRange:function(t){!t||!t.length?delete this.range:this.range=e.map(t,function(e){return e.valueOf()}),this.fill()},getClassNames:function(t){var n=[],r=this.viewDate.getUTCFullYear(),i=this.viewDate.getUTCMonth(),s=this.date.valueOf(),o=new Date;return t.getUTCFullYear()<r||t.getUTCFullYear()==r&&t.getUTCMonth()<i?n.push("old"):(t.getUTCFullYear()>r||t.getUTCFullYear()==r&&t.getUTCMonth()>i)&&n.push("new"),this.o.todayHighlight&&t.getUTCFullYear()==o.getFullYear()&&t.getUTCMonth()==o.getMonth()&&t.getUTCDate()==o.getDate()&&n.push("today"),s&&t.valueOf()==s&&n.push("active"),(t.valueOf()<this.o.startDate||t.valueOf()>this.o.endDate||e.inArray(t.getUTCDay(),this.o.daysOfWeekDisabled)!==-1)&&n.push("disabled"),this.range&&(t>this.range[0]&&t<this.range[this.range.length-1]&&n.push("range"),e.inArray(t.valueOf(),this.range)!=-1&&n.push("selected")),n},fill:function(){var n=new Date(this.viewDate),r=n.getUTCFullYear(),i=n.getUTCMonth(),s=this.o.startDate!==-Infinity?this.o.startDate.getUTCFullYear():-Infinity,o=this.o.startDate!==-Infinity?this.o.startDate.getUTCMonth():-Infinity,u=this.o.endDate!==Infinity?this.o.endDate.getUTCFullYear():Infinity,a=this.o.endDate!==Infinity?this.o.endDate.getUTCMonth():Infinity,f=this.date&&this.date.valueOf(),l;this.picker.find(".datepicker-days thead th.datepicker-switch").text(c[this.o.language].months[i]+" "+r),this.picker.find("tfoot th.today").text(c[this.o.language].today).toggle(this.o.todayBtn!==!1),this.picker.find("tfoot th.clear").text(c[this.o.language].clear).toggle(this.o.clearBtn!==!1),this.updateNavArrows(),this.fillMonths();var p=t(r,i-1,28,0,0,0,0),d=h.getDaysInMonth(p.getUTCFullYear(),p.getUTCMonth());p.setUTCDate(d),p.setUTCDate(d-(p.getUTCDay()-this.o.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.o.weekStart){m.push("<tr>");if(this.o.calendarWeeks){var y=new Date(+p+(this.o.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=this.getClassNames(p),g.push("day");var S=this.o.beforeShowDay(p);S===undefined?S={}:typeof S=="boolean"?S={enabled:S}:typeof S=="string"&&(S={classes:S}),S.enabled===!1&&g.push("disabled"),S.classes&&(g=g.concat(S.classes.split(/\s+/))),S.tooltip&&(l=S.tooltip),g=e.unique(g),m.push('<td class="'+g.join(" ")+'"'+(l?' title="'+l+'"':"")+">"+p.getUTCDate()+"</td>"),p.getUTCDay()==this.o.weekEnd&&m.push("</tr>"),p.setUTCDate(p.getUTCDate()+1)}this.picker.find(".datepicker-days tbody").empty().append(m.join(""));var x=this.date&&this.date.getUTCFullYear(),T=this.picker.find(".datepicker-months").find("th:eq(1)").text(r).end().find("span").removeClass("active");x&&x==r&&T.eq(this.date.getUTCMonth()).addClass("active"),(r<s||r>u)&&T.addClass("disabled"),r==s&&T.slice(0,o).addClass("disabled"),r==u&&T.slice(a+1).addClass("disabled"),m="",r=parseInt(r/10,10)*10;var N=this.picker.find(".datepicker-years").find("th:eq(1)").text(r+"-"+(r+9)).end().find("td");r-=1;for(var C=-1;C<11;C++)m+='<span class="year'+(C==-1?" old":C==10?" new":"")+(x==r?" active":"")+(r<s||r>u?" disabled":"")+'">'+r+"</span>",r+=1;N.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.o.startDate!==-Infinity&&t<=this.o.startDate.getUTCFullYear()&&n<=this.o.startDate.getUTCMonth()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),this.o.endDate!==Infinity&&t>=this.o.endDate.getUTCFullYear()&&n>=this.o.endDate.getUTCMonth()?this.picker.find(".next").css({visibility:"hidden"}):this.picker.find(".next").css({visibility:"visible"});break;case 1:case 2:this.o.startDate!==-Infinity&&t<=this.o.startDate.getUTCFullYear()?this.picker.find(".prev").css({visibility:"hidden"}):this.picker.find(".prev").css({visibility:"visible"}),this.o.endDate!==Infinity&&t>=this.o.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"datepicker-switch":this.showMode(1);break;case"prev":case"next":var i=h.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 s=new Date;s=t(s.getFullYear(),s.getMonth(),s.getDate(),0,0,0),this.showMode(-2);var o=this.o.todayBtn=="linked"?null:"view";this._setDate(s,o);break;case"clear":var u;this.isInput?u=this.element:this.component&&(u=this.element.find("input")),u&&u.val("").change(),this._trigger("changeDate"),this.update(),this.o.autoclose&&this.hide()}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._trigger("changeMonth",this.viewDate),this.o.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._trigger("changeYear",this.viewDate),this.o.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=new Date(e);if(!t||t=="view")this.viewDate=new Date(e);this.fill(),this.setValue(),this._trigger("changeDate");var n;this.isInput?n=this.element:this.component&&(n=this.element.find("input")),n&&(n.change(),this.o.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.o.startDate&&e<=this.o.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.o.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.o.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._trigger("changeDate");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.o.minViewMode,Math.min(2,this.viewMode+e))),this.picker.find(">div").hide().filter(".datepicker-"+h.modes[this.viewMode].clsName).css("display","block"),this.updateNavArrows()}};var i=function(t,n){this.element=e(t),this.inputs=e.map(n.inputs,function(e){return e.jquery?e[0]:e}),delete n.inputs,e(this.inputs).datepicker(n).bind("changeDate",e.proxy(this.dateUpdated,this)),this.pickers=e.map(this.inputs,function(t){return e(t).data("datepicker")}),this.updateDates()};i.prototype={updateDates:function(){this.dates=e.map(this.pickers,function(e){return e.date}),this.updateRanges()},updateRanges:function(){var t=e.map(this.dates,function(e){return e.valueOf()});e.each(this.pickers,function(e,n){n.setRange(t)})},dateUpdated:function(t){var n=e(t.target).data("datepicker"),r=n.getUTCDate(),i=e.inArray(t.target,this.inputs),s=this.inputs.length;if(i==-1)return;if(r<this.dates[i])while(i>=0&&r<this.dates[i])this.pickers[i--].setUTCDate(r);else if(r>this.dates[i])while(i<s&&r>this.dates[i])this.pickers[i++].setUTCDate(r);this.updateDates()},remove:function(){e.map(this.pickers,function(e){e.remove()}),delete this.element.data().datepicker}};var u=e.fn.datepicker,a=e.fn.datepicker=function(t){var n=Array.apply(null,arguments);n.shift();var u,a;return this.each(function(){var a=e(this),l=a.data("datepicker"),c=typeof t=="object"&&t;if(!l){var h=s(this,"date"),p=e.extend({},f,h,c),d=o(p.language),v=e.extend({},f,d,h,c);if(a.is(".input-daterange")||v.inputs){var m={inputs:v.inputs||a.find("input").toArray()};a.data("datepicker",l=new i(this,e.extend(v,m)))}else a.data("datepicker",l=new r(this,v))}if(typeof t=="string"&&typeof l[t]=="function"){u=l[t].apply(l,n);if(u!==undefined)return!1}}),u!==undefined?u:this},f=e.fn.datepicker.defaults={autoclose:!1,beforeShowDay:e.noop,calendarWeeks:!1,clearBtn:!1,daysOfWeekDisabled:[],endDate:Infinity,forceParse:!0,format:"mm/dd/yyyy",keyboardNavigation:!0,language:"en",minViewMode:0,rtl:!1,startDate:-Infinity,startView:0,todayBtn:!1,todayHighlight:!1,weekStart:0},l=e.fn.datepicker.locale_opts=["format","rtl","weekStart"];e.fn.datepicker.Constructor=r;var c=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",clear:"Clear"}},h={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,h.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,i,s){if(n instanceof Date)return n;typeof i=="string"&&(i=h.parseFormat(i));if(/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(n)){var o=/([\-+]\d+)([dmwy])/,u=n.match(/([\-+]\d+)([dmwy])/g),a,f;n=new Date;for(var l=0;l<u.length;l++){a=o.exec(u[l]),f=parseInt(a[1]);switch(a[2]){case"d":n.setUTCDate(n.getUTCDate()+f);break;case"m":n=r.prototype.moveMonth.call(r.prototype,n,f);break;case"w":n.setUTCDate(n.getUTCDate()+f*7);break;case"y":n=r.prototype.moveYear.call(r.prototype,n,f)}}return t(n.getUTCFullYear(),n.getUTCMonth(),n.getUTCDate(),0,0,0)}var u=n&&n.match(this.nonpunctuation)||[],n=new Date,p={},d=["yyyy","yy","M","MM","m","mm","d","dd"],v={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)}},m,g,a;v.M=v.MM=v.mm=v.m,v.dd=v.d,n=t(n.getFullYear(),n.getMonth(),n.getDate(),0,0,0);var y=i.parts.slice();u.length!=y.length&&(y=e(y).filter(function(t,n){return e.inArray(n,d)!==-1}).toArray());if(u.length==y.length){for(var l=0,b=y.length;l<b;l++){m=parseInt(u[l],10),a=y[l];if(isNaN(m))switch(a){case"MM":g=e(c[s].months).filter(function(){var e=this.slice(0,u[l].length),t=u[l].slice(0,e.length);return e==t}),m=e.inArray(g[0],c[s].months)+1;break;case"M":g=e(c[s].monthsShort).filter(function(){var e=this.slice(0,u[l].length),t=u[l].slice(0,e.length);return e==t}),m=e.inArray(g[0],c[s].monthsShort)+1}p[a]=m}for(var l=0,w;l<d.length;l++)w=d[l],w in p&&!isNaN(p[w])&&v[w](n,p[w])}return n},formatDate:function(t,n,r){typeof n=="string"&&(n=h.parseFormat(n));var i={d:t.getUTCDate(),D:c[r].daysShort[t.getUTCDay()],DD:c[r].days[t.getUTCDay()],m:t.getUTCMonth()+1,M:c[r].monthsShort[t.getUTCMonth()],MM:c[r].months[t.getUTCMonth()],yy:t.getUTCFullYear().toString().substring(2),yyyy:t.getUTCFullYear()};i.dd=(i.d<10?"0":"")+i.d,i.mm=(i.m<10?"0":"")+i.m;var t=[],s=e.extend([],n.separators);for(var o=0,u=n.parts.length;o<=u;o++)s.length&&t.push(s.shift()),t.push(i[n.parts[o]]);return t.join("")},headTemplate:'<thead><tr><th class="prev"><i class="icon-arrow-left"/></th><th colspan="5" class="datepicker-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><tr><th colspan="7" class="clear"></th></tr></tfoot>'};h.template='<div class="datepicker"><div class="datepicker-days"><table class=" table-condensed">'+h.headTemplate+"<tbody></tbody>"+h.footTemplate+"</table>"+"</div>"+'<div class="datepicker-months">'+'<table class="table-condensed">'+h.headTemplate+h.contTemplate+h.footTemplate+"</table>"+"</div>"+'<div class="datepicker-years">'+'<table class="table-condensed">'+h.headTemplate+h.contTemplate+h.footTemplate+"</table>"+"</div>"+"</div>",e.fn.datepicker.DPGlobal=h,e.fn.datepicker.noConflict=function(){return e.fn.datepicker=u,this},e(document).on("focus.datepicker.data-api click.datepicker.data-api",'[data-provide="datepicker"]',function(t){var n=e(this);if(n.data("datepicker"))return;t.preventDefault(),a.call(n,"show")}),e(function(){a.call(e('[data-provide="datepicker-inline"]'))})}(window.jQuery),function(e){"use strict";e.fn.bdatepicker=e.fn.datepicker.noConflict(),e.fn.datepicker||(e.fn.datepicker=e.fn.bdatepicker);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.bdatepicker.DPGlobal,this.parsedFormat=this.dpg.parseFormat(this.options.format),this.parsedViewFormat=this.dpg.parseFormat(this.options.viewformat)},render:function(){this.$input.bdatepicker(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 this.parseDate(e,this.parsedViewFormat)},value2str:function(e){return e?this.dpg.formatDate(e,this.parsedFormat,this.options.datepicker.language):""},str2value:function(e){return this.parseDate(e,this.parsedFormat)},value2submit:function(e){return this.value2str(e)},value2input:function(e){this.$input.bdatepicker("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)})},parseDate:function(e,t){var n=null,r;return e&&(n=this.dpg.parseDate(e,t,this.options.datepicker.language),typeof e=="string"&&(r=this.dpg.formatDate(n,t,this.options.datepicker.language),e!==r&&(n=null))),n}}),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.bdatepicker(this.options.datepicker),this.$input.off("focus keydown"),this.$input.keyup(e.proxy(function(){this.$tpl.removeData("date"),this.$tpl.bdatepicker("update")},this))},value2input:function(e){this.$input.val(e?this.dpg.formatDate(e,this.parsedViewFormat,this.options.datepicker.language):""),this.$tpl.bdatepicker("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){"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)},render:function(){this.$input.datetimepicker(this.options.datetimepicker),this.$input.on("changeMode",function(t){var n=e(this).closest("form").parent();setTimeout(function(){n.triggerHandler("resize")},0)}),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=this.parseDate(e,this.parsedViewFormat);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=this.parseDate(e,this.parsedFormat);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},parseDate:function(e,t){var n=null,r;return e&&(n=this.dpg.parseDate(e,t,this.options.datetimepicker.language,this.options.formatType),typeof e=="string"&&(r=this.dpg.formatDate(n,t,this.options.datetimepicker.language,this.options.formatType),e!==r&&(n=null))),n}}),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..4728a07 --- /dev/null +++ b/dist/jquery-editable/css/jquery-editable.css @@ -0,0 +1,201 @@ +/*! X-editable - v1.4.5 +* 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: '*'*/ +} + +.editable-bg-transition { + -webkit-transition: background-color 1400ms ease-out; + -moz-transition: background-color 1400ms ease-out; + -o-transition: background-color 1400ms ease-out; + -ms-transition: background-color 1400ms ease-out; + transition: background-color 1400ms ease-out; +} + +/*see https://github.com/vitalets/x-editable/issues/139 */ +.form-horizontal .editable +{ + padding-top: 5px; + display:inline-block; +} + 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..e776e82 --- /dev/null +++ b/dist/jquery-editable/js/jquery-editable-poshytip.js @@ -0,0 +1,4815 @@ +/*! X-editable - v1.4.5 +* 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(); + + //flag showing is form now saving value to server. + //It is needed to wait when closing form. + this.isSaving = false; + + /** + Fired when rendering starts + @event rendering + @param {Object} event event object + **/ + this.$div.triggerHandler('rendering'); + + //init input + this.initInput(); + + //append input to form + this.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; + } + + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + this.isSaving = true; + + //sending data to server + $.when(this.save(submitValue)) + .done($.proxy(function(response) { + this.isSaving = false; + + //run success callback + var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; + + //if success callback returns false --> keep form open and do not activate input + if(res === false) { + this.error(false); + this.showForm(false); + return; + } + + //if success callback returns string --> keep form open, show error and activate input + if(typeof res === 'string') { + this.error(res); + this.showForm(); + return; + } + + //if success callback returns object like {newValue: <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 raw new value + @param {mixed} params.submitValue submitted value as string + @param {Object} params.response ajax response + + @example + $('#form-div').on('save'), function(e, params){ + if(params.newValue === 'username') {...} + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); + }, this)) + .fail($.proxy(function(xhr) { + this.isSaving = false; + + var msg; + if(typeof this.options.error === 'function') { + msg = this.options.error.call(this.options.scope, xhr, newValue); + } else { + msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; + } + + this.error(msg); + this.showForm(); + }, this)); + }, + + save: function(submitValue) { + //try parse composite pk defined as json string in data-pk + this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); + + var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, + /* + send on server in following cases: + 1. url is function + 2. url is string AND (pk defined OR send option = always) + */ + send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), + params; + + if (send) { //send to server + this.showLoading(); + + //standard params + params = { + name: this.options.name || '', + value: submitValue, + pk: pk + }; + + //additional params + if(typeof this.options.params === 'function') { + params = this.options.params.call(this.options.scope, params); + } else { + //try parse json in single quotes (from data-params attribute) + this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); + $.extend(params, this.options.params); + } + + if(typeof this.options.url === 'function') { //user's function + return this.options.url.call(this.options.scope, params); + } else { + //send ajax to server and return deferred object + return $.ajax($.extend({ + url : this.options.url, + data : params, + type : 'POST' + }, this.options.ajaxOptions)); + } + } + }, + + validate: function (value) { + if (value === undefined) { + value = this.value; + } + if (typeof this.options.validate === 'function') { + return this.options.validate.call(this.options.scope, value); + } + }, + + option: function(key, value) { + if(key in this.options) { + this.options[key] = value; + } + + if(key === 'value') { + this.setValue(value); + } + + //do not pass option to input as it is passed in editable-element + }, + + setValue: function(value, convertStr) { + if(convertStr) { + this.value = this.input.str2value(value); + } else { + this.value = value; + } + + //if form is visible, update input + if(this.$form && this.$form.is(':visible')) { + this.input.value2input(this.value); + } + } + }; + + /* + Initialize editableform. Applied to jQuery object. + + @method $().editableform(options) + @params {Object} options + @example + var $form = $('<div>').editableform({ + type: 'text', + name: 'username', + url: '/post', + value: 'vitaliy' + }); + + //to display form you should call 'render' method + $form.editableform('render'); + */ + $.fn.editableform = function (option) { + var args = arguments; + return this.each(function () { + var $this = $(this), + data = $this.data('editableform'), + options = typeof option === 'object' && option; + if (!data) { + $this.data('editableform', (data = new EditableForm(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + //keep link to constructor to allow inheritance + $.fn.editableform.Constructor = EditableForm; + + //defaults + $.fn.editableform.defaults = { + /* see also defaults for input */ + + /** + Type of input. Can be <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; + } + }, + + //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr + supportsTransitions: function () { + var b = document.body || document.documentElement, + s = b.style, + p = 'transition', + v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; + + if(typeof s[p] === 'string') { + return true; + } + + // Tests for vendor specific prop + p = p.charAt(0).toUpperCase() + p.substr(1); + for(var i=0; i<v.length; i++) { + if(typeof s[v[i] + p] === 'string') { + return true; + } + } + 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(); + + //flag to hide container, when saving value will finish + this.delayedHide = false; + + //bind 'destroyed' listener to destroy container when element is removed from dom + this.$element.on('destroyed', $.proxy(function(){ + this.destroy(); + }, this)); + + //attach document handler to close containers on click / escape + if(!$(document).data('editable-handlers-attached')) { + //close all on escape + $(document).on('keyup.editable', function (e) { + if (e.which === 27) { + $('.editable-open').editableContainer('hide'); + //todo: return focus on element + } + }); + + //close containers when click outside + //(mousedown could be better than click, it closes everything also on drag drop) + $(document).on('click.editable', function(e) { + var $target = $(e.target), i, + exclude_classes = ['.editable-container', + '.ui-datepicker-header', + '.datepicker', //in inline mode datepicker is rendered into body + '.modal-backdrop', + '.bootstrap-wysihtml5-insert-image-modal', + '.bootstrap-wysihtml5-insert-link-modal' + ]; + + //check if element is detached. It occurs when clicking in bootstrap datepicker + if (!$.contains(document.documentElement, e.target)) { + return; + } + + //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document + //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 + //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec + if($target.is(document)) { + return; + } + + //if click inside one of exclude classes --> no nothing + for(i=0; i<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(function() { + if(this.delayedHide) { + this.hide(this.delayedHide.reason); + this.delayedHide = false; + } else { + 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 will container do with content when closed: + remove(), detach() or just hide() - it depends on container. + + Detaching form itself before hide and re-insert before show is good solution, + but visually it looks ugly --> 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; + } + + //if form is saving value, schedule hide + if(this.$form.data('editableform').isSaving) { + this.delayedHide = {reason: reason}; + return; + } else { + this.delayedHide = false; + } + + this.$element.removeClass('editable-open'); + this.innerHide(); + + /** + Fired when container was hidden. It occurs on both save or cancel. + **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. + The workaround is to check `arguments.length` that is always `2` for x-editable. + + @event hidden + @param {object} event event object + @param {string} reason Reason caused hiding. Can be <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 of plugin, applied to input + this.hide('save'); + }, + + /** + Sets new option + + @method option(key, value) + @param {string} key + @param {mixed} value + **/ + option: function(key, value) { + this.options[key] = value; + if(key in this.containerOptions) { + this.containerOptions[key] = value; + this.setContainerOption(key, value); + } else { + this.formOptions[key] = value; + if(this.$form) { + this.$form.editableform('option', key, value); + } + } + }, + + setContainerOption: function(key, value) { + this.call('option', key, value); + }, + + /** + Destroys the container instance + @method destroy() + **/ + destroy: function() { + this.hide(); + this.innerDestroy(); + this.$element.off('destroyed'); + this.$element.removeData('editableContainer'); + }, + + /* to be overwritten in child classes */ + innerDestroy: function() { + + }, + + /* + Closes other containers except one related to passed element. + Other containers can be cancelled or submitted (depends on onblur option) + */ + closeOthers: function(element) { + $('.editable-open').each(function(i, el){ + //do nothing with passed element and it's children + if(el === element || $(el).find(element).length) { + return; + } + + //otherwise cancel or submit all open containers + var $el = $(el), + ec = $el.data('editableContainer'); + + if(!ec) { + return; + } + + if(ec.options.onblur === 'cancel') { + $el.data('editableContainer').hide('onblur'); + } else if(ec.options.onblur === 'submit') { + $el.data('editableContainer').tip().find('form').submit(); + } + }); + + }, + + /** + Activates input of visible container (e.g. set focus) + @method activate() + **/ + activate: function() { + if(this.tip && this.tip().is(':visible') && this.$form) { + this.$form.data('editableform').input.activate(); + } + } + + }; + + /** + jQuery method to initialize editableContainer. + + @method $().editableContainer(options) + @params {Object} options + @example + $('#edit').editableContainer({ + type: 'text', + url: '/post', + pk: 1, + value: 'hello' + }); + **/ + $.fn.editableContainer = function (option) { + var args = arguments; + return this.each(function () { + var $this = $(this), + dataKey = 'editableContainer', + data = $this.data(dataKey), + options = typeof option === 'object' && option, + Constructor = (options.mode === 'inline') ? Inline : Popup; + + if (!data) { + $this.data(dataKey, (data = new Constructor(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + //store constructors + $.fn.editableContainer.Popup = Popup; + $.fn.editableContainer.Inline = Inline; + + //defaults + $.fn.editableContainer.defaults = { + /** + Initial value of form input + + @property value + @type mixed + @default null + @private + **/ + value: null, + /** + Placement of container relative to element. Can be <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 only) + @property anim + @type string + @default false + **/ + anim: false, + + /** + Mode of editable, can be `popup` or `inline` + + @property mode + @type string + @default 'popup' + @since 1.4.0 + **/ + mode: 'popup' + }; + + /* + * workaround to have 'destroyed' event to destroy popover when element is destroyed + * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom + */ + jQuery.event.special.destroyed = { + remove: function(o) { + if (o.handler) { + o.handler(); + } + } + }; + +}(window.jQuery)); + +/** +* Editable Inline +* --------------------- +*/ +(function ($) { + "use strict"; + + //copy prototype from EditableContainer + //extend methods + $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { + containerName: 'editableform', + innerCss: '.editable-inline', + containerClass: 'editable-container editable-inline', //css class applied to container element + + initContainer: function(){ + //container is <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(); + } + + //check for transition support + if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { + this.options.highlight = false; + } + }; + + Editable.prototype = { + constructor: Editable, + init: function () { + var isValueByText = false, + doAutotext, finalize; + + //name + this.options.name = this.options.name || this.$element.attr('id'); + + //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select) + //also we set scope option to have access to element inside input specific callbacks (e. g. source as function) + this.options.scope = this.$element[0]; + this.input = $.fn.editableutils.createInput(this.options); + if(!this.input) { + return; + } + + //set value from settings or by element's text + if (this.options.value === undefined || this.options.value === null) { + this.value = this.input.html2value($.trim(this.$element.html())); + isValueByText = true; + } else { + /* + value can be string when received from 'data-value' attribute + for complext objects value can be set as json string in data-value attribute, + e.g. data-value="{city: 'Moscow', street: 'Lenina'}" + */ + this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); + if(typeof this.options.value === 'string') { + this.value = this.input.str2value(this.options.value); + } else { + this.value = this.options.value; + } + } + + //add 'editable' class to every editable element + this.$element.addClass('editable'); + + //attach handler activating editable. In disabled mode it just prevent default action (useful for links) + if(this.options.toggle !== 'manual') { + this.$element.addClass('editable-click'); + this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ + //prevent following link if editable enabled + if(!this.options.disabled) { + e.preventDefault(); + } + + //stop propagation not required because in document click handler it checks event target + //e.stopPropagation(); + + if(this.options.toggle === 'mouseenter') { + //for hover only show container + this.show(); + } else { + //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener + var closeAll = (this.options.toggle !== 'click'); + this.toggle(closeAll); + } + }, this)); + } else { + this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually + } + + //if display is function it's far more convinient to have autotext = always to render correctly on init + //see https://github.com/vitalets/x-editable-yii/issues/34 + if(typeof this.options.display === 'function') { + this.options.autotext = 'always'; + } + + //check conditions for autotext: + switch(this.options.autotext) { + case 'always': + doAutotext = true; + break; + case 'auto': + //if element text is empty and value is defined and value not generated by text --> run autotext + doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText; + break; + default: + doAutotext = false; + } + + //depending on autotext run render() or just finilize init + $.when(doAutotext ? this.render() : true).then($.proxy(function() { + if(this.options.disabled) { + this.disable(); + } else { + this.enable(); + } + /** + Fired when element was initialized by `$().editable()` method. + Please note that you should setup `init` handler **before** applying `editable`. + + @event init + @param {Object} event event object + @param {Object} editable editable instance (as here it cannot accessed via data('editable')) + @since 1.2.0 + @example + $('#username').on('init', function(e, editable) { + alert('initialized ' + editable.options.name); + }); + $('#username').editable(); + **/ + this.$element.triggerHandler('init', this); + }, this)); + }, + + /* + Initializes parent element for live editables + */ + initLive: function() { + //store selector + var selector = this.options.selector; + //modify options for child elements + this.options.selector = false; + this.options.autotext = 'never'; + //listen toggle events + this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ + var $target = $(e.target); + if(!$target.data('editable')) { + //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user) + //see https://github.com/vitalets/x-editable/issues/137 + if($target.hasClass(this.options.emptyclass)) { + $target.empty(); + } + $target.editable(this.options).trigger(e); + } + }, this)); + }, + + /* + Renders value into element's text. + Can call custom display method from options. + Can return deferred object. + @method render() + @param {mixed} response server response (if exist) to pass into display function + */ + render: function(response) { + //do not display anything + if(this.options.display === false) { + return; + } + + //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded + if(this.input.value2htmlFinal) { + return this.input.value2html(this.value, this.$element[0], this.options.display, response); + //if display method defined --> use it + } else if(typeof this.options.display === 'function') { + return this.options.display.call(this.$element[0], this.value, response); + //else use input's original value2html() method + } else { + return this.input.value2html(this.value, this.$element[0]); + } + }, + + /** + Enables editable + @method enable() + **/ + enable: function() { + this.options.disabled = false; + this.$element.removeClass('editable-disabled'); + this.handleEmpty(this.isEmpty); + if(this.options.toggle !== 'manual') { + if(this.$element.attr('tabindex') === '-1') { + this.$element.removeAttr('tabindex'); + } + } + }, + + /** + Disables editable + @method disable() + **/ + disable: function() { + this.options.disabled = true; + this.hide(); + this.$element.addClass('editable-disabled'); + this.handleEmpty(this.isEmpty); + //do not stop focus on this element + this.$element.attr('tabindex', -1); + }, + + /** + Toggles enabled / disabled state of editable element + @method toggleDisabled() + **/ + toggleDisabled: function() { + if(this.options.disabled) { + this.enable(); + } else { + this.disable(); + } + }, + + /** + Sets new option + + @method option(key, value) + @param {string|object} key option name or object with several options + @param {mixed} value option new value + @example + $('.editable').editable('option', 'pk', 2); + **/ + option: function(key, value) { + //set option(s) by object + if(key && typeof key === 'object') { + $.each(key, $.proxy(function(k, v){ + this.option($.trim(k), v); + }, this)); + return; + } + + //set option by string + this.options[key] = value; + + //disabled + if(key === 'disabled') { + return value ? this.disable() : this.enable(); + } + + //value + if(key === 'value') { + this.setValue(value); + } + + //transfer new option to container! + if(this.container) { + this.container.option(key, value); + } + + //pass option to input directly (as it points to the same in form) + if(this.input.option) { + this.input.option(key, value); + } + + }, + + /* + * set emptytext if element is empty + */ + handleEmpty: function (isEmpty) { + //do not handle empty if we do not display anything + if(this.options.display === false) { + return; + } + + /* + isEmpty may be set directly as param of method. + It is required when we enable/disable field and can't rely on content + as node content is text: "Empty" that is not empty %) + */ + if(isEmpty !== undefined) { + this.isEmpty = isEmpty; + } else { + //detect empty + if($.trim(this.$element.html()) === '') { + this.isEmpty = true; + } else if($.trim(this.$element.text()) !== '') { + this.isEmpty = false; + } else { + //e.g. '<img>' + this.isEmpty = !this.$element.height() || !this.$element.width(); + } + } + + //emptytext shown only for enabled + if(!this.options.disabled) { + if (this.isEmpty) { + this.$element.html(this.options.emptytext); + if(this.options.emptyclass) { + this.$element.addClass(this.options.emptyclass); + } + } else if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } + } else { + //below required if element disable property was changed + if(this.isEmpty) { + this.$element.empty(); + if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } + } + } + }, + + /** + Shows container with form + @method show() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + show: function (closeAll) { + if(this.options.disabled) { + return; + } + + //init editableContainer: popover, tooltip, inline, etc.. + if(!this.container) { + var containerOptions = $.extend({}, this.options, { + value: this.value, + input: this.input //pass input to form (as it is already created) + }); + this.$element.editableContainer(containerOptions); + //listen `save` event + this.$element.on("save.internal", $.proxy(this.save, this)); + this.container = this.$element.data('editableContainer'); + } else if(this.container.tip().is(':visible')) { + return; + } + + //show container + this.container.show(closeAll); + }, + + /** + Hides container with form + @method hide() + **/ + hide: function () { + if(this.container) { + this.container.hide(); + } + }, + + /** + Toggles container visibility (show / hide) + @method toggle() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + toggle: function(closeAll) { + if(this.container && this.container.tip().is(':visible')) { + this.hide(); + } else { + this.show(closeAll); + } + }, + + /* + * called when form was submitted + */ + save: function(e, params) { + //mark element with unsaved class if needed + if(this.options.unsavedclass) { + /* + Add unsaved css to element if: + - url is not user's function + - value was not sent to server + - params.response === undefined, that means data was not sent + - value changed + */ + var sent = false; + sent = sent || typeof this.options.url === 'function'; + sent = sent || this.options.display === false; + sent = sent || params.response !== undefined; + sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); + + if(sent) { + this.$element.removeClass(this.options.unsavedclass); + } else { + this.$element.addClass(this.options.unsavedclass); + } + } + + //highlight when saving + if(this.options.highlight) { + var $e = this.$element, + $bgColor = $e.css('background-color'); + + $e.css('background-color', this.options.highlight); + setTimeout(function(){ + $e.css('background-color', $bgColor); + $e.addClass('editable-bg-transition'); + setTimeout(function(){ + $e.removeClass('editable-bg-transition'); + }, 1700); + }, 0); + } + + //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(); + } + + this.input.destroy(); + + if(this.options.toggle !== 'manual') { + this.$element.removeClass('editable-click'); + this.$element.off(this.options.toggle + '.editable'); + } + + this.$element.off("save.internal"); + + this.$element.removeClass('editable editable-open editable-disabled'); + this.$element.removeData('editable'); + } + }; + + /* EDITABLE PLUGIN DEFINITION + * ======================= */ + + /** + jQuery method to initialize editable element. + + @method $().editable(options) + @params {Object} options + @example + $('#username').editable({ + type: 'text', + url: '/post', + pk: 1 + }); + **/ + $.fn.editable = function (option) { + //special API methods returning non-jquery object + var result = {}, args = arguments, datakey = 'editable'; + switch (option) { + /** + Runs client-side validation for all matched editables + + @method validate() + @returns {Object} validation errors map + @example + $('#username, #fullname').editable('validate'); + // possible result: + { + username: "username is required", + fullname: "fullname should be minimum 3 letters length" + } + **/ + case 'validate': + this.each(function () { + var $this = $(this), data = $this.data(datakey), error; + if (data && (error = data.validate())) { + result[data.options.name] = error; + } + }); + return result; + + /** + Returns current values of editable elements. + Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. + If value of some editable is `null` or `undefined` it is excluded from result object. + When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. + + @method getValue() + @param {bool} isSingle whether to return just value of single element + @returns {Object} object of element names and values + @example + $('#username, #fullname').editable('getValue'); + //result: + { + username: "superuser", + fullname: "John" + } + //isSingle = true + $('#username').editable('getValue', true); + //result "superuser" + **/ + case 'getValue': + if(arguments.length === 2 && arguments[1] === true) { //isSingle = true + result = this.eq(0).data(datakey).value; + } else { + this.each(function () { + var $this = $(this), data = $this.data(datakey); + if (data && data.value !== undefined && data.value !== null) { + result[data.options.name] = data.input.value2submit(data.value); + } + }); + } + return result; + + /** + This method collects values from several editable elements and submit them all to server. + Internally it runs client-side validation for all fields and submits only in case of success. + See <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, + /** + Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. + + @property highlight + @type string|boolean + @since 1.4.5 + @default #FFFF80 + **/ + highlight: '#FFFF80' + }; + +}(window.jQuery)); + +/** +AbstractInput - base class for all editable inputs. +It defines interface to be implemented by any input type. +To create your own input you can inherit from this class. + +@class abstractinput +**/ +(function ($) { + "use strict"; + + //types + $.fn.editabletypes = {}; + + var AbstractInput = function () { }; + + AbstractInput.prototype = { + /** + Initializes input + + @method init() + **/ + init: function(type, options, defaults) { + this.type = type; + this.options = $.extend({}, defaults, options); + }, + + /* + this method called before render to init $tpl that is inserted in DOM + */ + prerender: function() { + this.$tpl = $(this.options.tpl); //whole tpl as jquery object + this.$input = this.$tpl; //control itself, can be changed in render method + this.$clear = null; //clear button + this.error = null; //error message, if input cannot be rendered + }, + + /** + Renders input from tpl. Can return jQuery deferred object. + Can be overwritten in child objects + + @method render() + **/ + render: function() { + + }, + + /** + Sets element's html by value. + + @method value2html(value, element) + @param {mixed} value + @param {DOMElement} element + **/ + value2html: function(value, element) { + $(element).text($.trim(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() { + + }, + + /** + Additional actions when destroying element + **/ + destroy: function() { + }, + + // -------- helper functions -------- + setClass: function() { + if(this.options.inputclass) { + this.$input.addClass(this.options.inputclass); + } + }, + + setAttr: function(attr) { + if (this.options[attr] !== undefined && this.options[attr] !== null) { + this.$input.attr(attr, this.options[attr]); + } + }, + + option: function(key, value) { + this.options[key] = value; + } + + }; + + AbstractInput.defaults = { + /** + HTML template of input. Normally you should not change it. + + @property tpl + @type string + @default '' + **/ + tpl: '', + /** + CSS class automatically applied to input + + @property inputclass + @type string + @default 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) { + //run source if it function + var source; + if ($.isFunction(this.options.source)) { + source = this.options.source.call(this.options.scope); + this.sourceData = null; + //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed + } else { + source = this.options.source; + } + + //if allready loaded just call success + if(this.options.sourceCache && $.isArray(this.sourceData)) { + success.call(this); + return; + } + + //try parse json in single quotes (for double quotes jquery does automatically) + try { + source = $.fn.editableutils.tryParseJson(source, false); + } catch (e) { + error.call(this); + return; + } + + //loading from url + if (typeof source === 'string') { + //try to get sourceData from cache + if(this.options.sourceCache) { + var cacheID = source, + cache; + + if (!$(document).data(cacheID)) { + $(document).data(cacheID, {}); + } + cache = $(document).data(cacheID); + + //check for cached data + if (cache.loading === false && cache.sourceData) { //take source from cache + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + return; + } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later + cache.callbacks.push($.proxy(function () { + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + }, this)); + + //also collecting error callbacks + cache.err_callbacks.push($.proxy(error, this)); + return; + } else { //no cache yet, activate it + cache.loading = true; + cache.callbacks = []; + cache.err_callbacks = []; + } + } + + //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 select2 docs](http://ivaynberg.github.com/select2) for detailed description and options. +Compatible **select2 version is 3.4.1**! +You should manually download and include select2 distributive: + + <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> + <script src="select2/select2.js"></script> + +To 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 `autotext` feature does not work for select2 with `ajax` remote source. +You need initially put both `data-value` and element's text youself: + + <a href="#" data-type="select2" data-value="1">Text1</a> + + +@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-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 || {}; + + this.sourceData = null; + + //placeholder + if(options.placeholder) { + options.select2.placeholder = options.placeholder; + } + + //if not `tags` mode, use source + if(!options.select2.tags && options.source) { + var source = options.source; + //if source is function, call it (once!) + if ($.isFunction(options.source)) { + source = options.source.call(options.scope); + } + + if (typeof source === 'string') { + options.select2.ajax = options.select2.ajax || {}; + //some default ajax params + if(!options.select2.ajax.data) { + options.select2.ajax.data = function(term) {return { query:term };}; + } + if(!options.select2.ajax.results) { + options.select2.ajax.results = function(data) { return {results:data };}; + } + options.select2.ajax.url = source; + } else { + //check format and convert x-editable format to select2 format (if needed) + this.sourceData = this.convertSource(source); + options.select2.data = this.sourceData; + } + } + + //overriding objects in config (as by default jQuery extend() is not recursive) + this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2); + + //detect whether it is multi-valued + this.isMultiple = this.options.select2.tags || this.options.select2.multiple; + this.isRemote = ('ajax' in this.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 to populate listData + if(this.isRemote) { + //listen to loaded event to populate data + this.$input.on('select2-loaded', $.proxy(function(e) { + this.sourceData = e.items.results; + }, this)); + } + + //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.options.select2.tags) { //in tags mode just assign value + data = value; + } else if(this.sourceData) { + data = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + } else { + //can not get list of possible values (e.g. autotext for select2 with ajax source) + } + + //data may be array (when multiple values allowed) + 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) { + //for remote source .val() is not working, need to look in sourceData + if(this.isRemote) { + //todo: check value for array + var item, items; + //if sourceData loaded, use it to get text for display + if(this.sourceData) { + items = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + if(items.length) { + item = items[0]; + } + } + //if item not found by sourceData, use element text (e.g. for the first show) + if(!item) { + item = {id: value, text: $(this.options.scope).text()}; + } + //select2('data', ...) allows to set both id and text --> usefull for initial show when items are not loaded + this.$input.select2('data', item).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + } else { + 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(); + } + }); + }, + + /* + Converts source from x-editable format: {value: 1, text: "1"} to + select2 format: {id: 1, text: "1"} + */ + convertSource: function(source) { + if($.isArray(source) && source.length && source[0].value !== undefined) { + for(var i = 0; i<source.length; i++) { + if(source[i].value !== undefined) { + source[i].id = source[i].value; + delete source[i].value; + } + } + } + return source; + } + + }); + + 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.4 +* 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 +* +* Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight +* In combodate: +* 12:00 pm --> 12:00 (24-h format, midday) +* 12:00 am --> 00:00 (24-h format, midnight, start of day) +* +* Differs from momentjs parse rules: +* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change) +* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change) +* +* +* 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) { + //see https://github.com/timrwood/momentjs.com/pull/36 + name = moment().date(1).month(i).format('MMMM'); + } else if(shortNames) { + name = moment().date(1).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, + min = h12 ? 1 : 0, + max = h12 ? 12 : 23; + + for(i=min; 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 from combos. + 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 12h --> 24h + if(this.$ampm) { + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour === 12) { + values.hour = this.$ampm.val() === 'am' ? 0 : 12; + } else { + values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; + } + } + + 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) { + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour >= 12) { + values.ampm = 'pm'; + if(values.hour > 12) { + values.hour -= 12; + } + } else { + values.ampm = 'am'; + if(values.hour === 0) { + values.hour = 12; + } + } + } + + $.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(this.containerClass); + 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..65292a7 --- /dev/null +++ b/dist/jquery-editable/js/jquery-editable-poshytip.min.js @@ -0,0 +1,5 @@ +/*! X-editable - v1.4.5 +* 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.isSaving=!1,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}var i=this.input.value2submit(r);this.isSaving=!0,e.when(this.save(i)).done(e.proxy(function(e){this.isSaving=!1;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,submitValue:i,response:e})},this)).fail(e.proxy(function(e){this.isSaving=!1;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){this.options.pk=e.fn.editableutils.tryParseJson(this.options.pk,!0);var n=typeof this.options.pk=="function"?this.options.pk.call(this.options.scope):this.options.pk,r=!!(typeof this.options.url=="function"||this.options.url&&(this.options.send==="always"||this.options.send==="auto"&&n!==null&&n!==undefined)),i;if(r)return this.showLoading(),i={name:this.options.name||"",value:t,pk:n},typeof this.options.params=="function"?i=this.options.params.call(this.options.scope,i):(this.options.params=e.fn.editableutils.tryParseJson(this.options.params,!0),e.extend(i,this.options.params)),typeof this.options.url=="function"?this.options.url.call(this.options.scope,i):e.ajax(e.extend({url:this.options.url,data:i,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)},supportsTransitions:function(){var e=document.body||document.documentElement,t=e.style,n="transition",r=["Moz","Webkit","Khtml","O","ms"];if(typeof t[n]=="string")return!0;n=n.charAt(0).toUpperCase()+n.substr(1);for(var i=0;i<r.length;i++)if(typeof t[r[i]+n]=="string")return!0;return!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.delayedHide=!1,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(function(){this.delayedHide?(this.hide(this.delayedHide.reason),this.delayedHide=!1):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;if(this.$form.data("editableform").isSaving){this.delayedHide={reason:e};return}this.delayedHide=!1,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(),this.options.highlight&&!e.fn.editableutils.supportsTransitions()&&(this.options.highlight=!1)};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){this.options.disabled||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),typeof this.options.display=="function"&&(this.options.autotext="always");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;t!==undefined?this.isEmpty=t:e.trim(this.$element.html())===""?this.isEmpty=!0:e.trim(this.$element.text())!==""?this.isEmpty=!1:this.isEmpty=!this.$element.height()||!this.$element.width(),this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.html(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)}if(this.options.highlight){var r=this.$element,i=r.css("background-color");r.css("background-color",this.options.highlight),setTimeout(function(){r.css("background-color",i),r.addClass("editable-bg-transition"),setTimeout(function(){r.removeClass("editable-bg-transition")},1700)},0)}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.input.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 arguments.length===2&&arguments[1]===!0?r=this.eq(0).data(s).value: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,highlight:"#FFFF80"}}(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(e.trim(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(){},destroy: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){var r;e.isFunction(this.options.source)?(r=this.options.source.call(this.options.scope),this.sourceData=null):r=this.options.source;if(this.options.sourceCache&&e.isArray(this.sourceData)){t.call(this);return}try{r=e.fn.editableutils.tryParseJson(r,!1)}catch(i){n.call(this);return}if(typeof r=="string"){if(this.options.sourceCache){var s=r,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:r,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(r),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||{},this.sourceData=null,n.placeholder&&(n.select2.placeholder=n.placeholder);if(!n.select2.tags&&n.source){var r=n.source;e.isFunction(n.source)&&(r=n.source.call(n.scope)),typeof r=="string"?(n.select2.ajax=n.select2.ajax||{},n.select2.ajax.data||(n.select2.ajax.data=function(e){return{query:e}}),n.select2.ajax.results||(n.select2.ajax.results=function(e){return{results:e}}),n.select2.ajax.url=r):(this.sourceData=this.convertSource(r),n.select2.data=this.sourceData)}this.options.select2=e.extend({},t.defaults.select2,n.select2),this.isMultiple=this.options.select2.tags||this.options.select2.multiple,this.isRemote="ajax"in this.options.select2};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.$input.select2(this.options.select2),this.isRemote&&this.$input.on("select2-loaded",e.proxy(function(e){this.sourceData=e.items.results},this)),this.isMultiple&&this.$input.on("change",function(){e(this).closest("form").parent().triggerHandler("resize")})},value2html:function(t,n){var r="",i;this.options.select2.tags?i=t:this.sourceData&&(i=e.fn.editableutils.itemsByValue(t,this.sourceData,"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(t){if(this.isRemote){var n,r;this.sourceData&&(r=e.fn.editableutils.itemsByValue(t,this.sourceData,"id"),r.length&&(n=r[0])),n||(n={id:t,text:e(this.options.scope).text()}),this.$input.select2("data",n).trigger("change",!0)}else this.$input.val(t).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()})},convertSource:function(t){if(e.isArray(t)&&t.length&&t[0].value!==undefined)for(var n=0;n<t.length;n++)t[n].value!==undefined&&(t[n].id=t[n].value,delete t[n].value);return t}}),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().date(1).month(n).format("MMMM"):i?t=moment().date(1).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?1:0,u=r?12:23;for(n=o;n<=u;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===12?r.hour=this.$ampm.val()==="am"?0:12:r.hour=this.$ampm.val()==="am"?r.hour:r.hour+12),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.ampm="pm",i.hour>12&&(i.hour-=12)):(i.ampm="am",i.hour===0&&(i.hour=12))),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(this.containerClass),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..4728a07 --- /dev/null +++ b/dist/jqueryui-editable/css/jqueryui-editable.css @@ -0,0 +1,201 @@ +/*! X-editable - v1.4.5 +* 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: '*'*/ +} + +.editable-bg-transition { + -webkit-transition: background-color 1400ms ease-out; + -moz-transition: background-color 1400ms ease-out; + -o-transition: background-color 1400ms ease-out; + -ms-transition: background-color 1400ms ease-out; + transition: background-color 1400ms ease-out; +} + +/*see https://github.com/vitalets/x-editable/issues/139 */ +.form-horizontal .editable +{ + padding-top: 5px; + display:inline-block; +} + 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..ffb5e92 --- /dev/null +++ b/dist/jqueryui-editable/js/jqueryui-editable.js @@ -0,0 +1,4749 @@ +/*! X-editable - v1.4.5 +* 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(); + + //flag showing is form now saving value to server. + //It is needed to wait when closing form. + this.isSaving = false; + + /** + Fired when rendering starts + @event rendering + @param {Object} event event object + **/ + this.$div.triggerHandler('rendering'); + + //init input + this.initInput(); + + //append input to form + this.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; + } + + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + this.isSaving = true; + + //sending data to server + $.when(this.save(submitValue)) + .done($.proxy(function(response) { + this.isSaving = false; + + //run success callback + var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; + + //if success callback returns false --> keep form open and do not activate input + if(res === false) { + this.error(false); + this.showForm(false); + return; + } + + //if success callback returns string --> keep form open, show error and activate input + if(typeof res === 'string') { + this.error(res); + this.showForm(); + return; + } + + //if success callback returns object like {newValue: <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 raw new value + @param {mixed} params.submitValue submitted value as string + @param {Object} params.response ajax response + + @example + $('#form-div').on('save'), function(e, params){ + if(params.newValue === 'username') {...} + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); + }, this)) + .fail($.proxy(function(xhr) { + this.isSaving = false; + + var msg; + if(typeof this.options.error === 'function') { + msg = this.options.error.call(this.options.scope, xhr, newValue); + } else { + msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; + } + + this.error(msg); + this.showForm(); + }, this)); + }, + + save: function(submitValue) { + //try parse composite pk defined as json string in data-pk + this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); + + var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, + /* + send on server in following cases: + 1. url is function + 2. url is string AND (pk defined OR send option = always) + */ + send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), + params; + + if (send) { //send to server + this.showLoading(); + + //standard params + params = { + name: this.options.name || '', + value: submitValue, + pk: pk + }; + + //additional params + if(typeof this.options.params === 'function') { + params = this.options.params.call(this.options.scope, params); + } else { + //try parse json in single quotes (from data-params attribute) + this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); + $.extend(params, this.options.params); + } + + if(typeof this.options.url === 'function') { //user's function + return this.options.url.call(this.options.scope, params); + } else { + //send ajax to server and return deferred object + return $.ajax($.extend({ + url : this.options.url, + data : params, + type : 'POST' + }, this.options.ajaxOptions)); + } + } + }, + + validate: function (value) { + if (value === undefined) { + value = this.value; + } + if (typeof this.options.validate === 'function') { + return this.options.validate.call(this.options.scope, value); + } + }, + + option: function(key, value) { + if(key in this.options) { + this.options[key] = value; + } + + if(key === 'value') { + this.setValue(value); + } + + //do not pass option to input as it is passed in editable-element + }, + + setValue: function(value, convertStr) { + if(convertStr) { + this.value = this.input.str2value(value); + } else { + this.value = value; + } + + //if form is visible, update input + if(this.$form && this.$form.is(':visible')) { + this.input.value2input(this.value); + } + } + }; + + /* + Initialize editableform. Applied to jQuery object. + + @method $().editableform(options) + @params {Object} options + @example + var $form = $('<div>').editableform({ + type: 'text', + name: 'username', + url: '/post', + value: 'vitaliy' + }); + + //to display form you should call 'render' method + $form.editableform('render'); + */ + $.fn.editableform = function (option) { + var args = arguments; + return this.each(function () { + var $this = $(this), + data = $this.data('editableform'), + options = typeof option === 'object' && option; + if (!data) { + $this.data('editableform', (data = new EditableForm(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + //keep link to constructor to allow inheritance + $.fn.editableform.Constructor = EditableForm; + + //defaults + $.fn.editableform.defaults = { + /* see also defaults for input */ + + /** + Type of input. Can be <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; + } + }, + + //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr + supportsTransitions: function () { + var b = document.body || document.documentElement, + s = b.style, + p = 'transition', + v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; + + if(typeof s[p] === 'string') { + return true; + } + + // Tests for vendor specific prop + p = p.charAt(0).toUpperCase() + p.substr(1); + for(var i=0; i<v.length; i++) { + if(typeof s[v[i] + p] === 'string') { + return true; + } + } + 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(); + + //flag to hide container, when saving value will finish + this.delayedHide = false; + + //bind 'destroyed' listener to destroy container when element is removed from dom + this.$element.on('destroyed', $.proxy(function(){ + this.destroy(); + }, this)); + + //attach document handler to close containers on click / escape + if(!$(document).data('editable-handlers-attached')) { + //close all on escape + $(document).on('keyup.editable', function (e) { + if (e.which === 27) { + $('.editable-open').editableContainer('hide'); + //todo: return focus on element + } + }); + + //close containers when click outside + //(mousedown could be better than click, it closes everything also on drag drop) + $(document).on('click.editable', function(e) { + var $target = $(e.target), i, + exclude_classes = ['.editable-container', + '.ui-datepicker-header', + '.datepicker', //in inline mode datepicker is rendered into body + '.modal-backdrop', + '.bootstrap-wysihtml5-insert-image-modal', + '.bootstrap-wysihtml5-insert-link-modal' + ]; + + //check if element is detached. It occurs when clicking in bootstrap datepicker + if (!$.contains(document.documentElement, e.target)) { + return; + } + + //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document + //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 + //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec + if($target.is(document)) { + return; + } + + //if click inside one of exclude classes --> no nothing + for(i=0; i<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(function() { + if(this.delayedHide) { + this.hide(this.delayedHide.reason); + this.delayedHide = false; + } else { + 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 will container do with content when closed: + remove(), detach() or just hide() - it depends on container. + + Detaching form itself before hide and re-insert before show is good solution, + but visually it looks ugly --> 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; + } + + //if form is saving value, schedule hide + if(this.$form.data('editableform').isSaving) { + this.delayedHide = {reason: reason}; + return; + } else { + this.delayedHide = false; + } + + this.$element.removeClass('editable-open'); + this.innerHide(); + + /** + Fired when container was hidden. It occurs on both save or cancel. + **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. + The workaround is to check `arguments.length` that is always `2` for x-editable. + + @event hidden + @param {object} event event object + @param {string} reason Reason caused hiding. Can be <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 of plugin, applied to input + this.hide('save'); + }, + + /** + Sets new option + + @method option(key, value) + @param {string} key + @param {mixed} value + **/ + option: function(key, value) { + this.options[key] = value; + if(key in this.containerOptions) { + this.containerOptions[key] = value; + this.setContainerOption(key, value); + } else { + this.formOptions[key] = value; + if(this.$form) { + this.$form.editableform('option', key, value); + } + } + }, + + setContainerOption: function(key, value) { + this.call('option', key, value); + }, + + /** + Destroys the container instance + @method destroy() + **/ + destroy: function() { + this.hide(); + this.innerDestroy(); + this.$element.off('destroyed'); + this.$element.removeData('editableContainer'); + }, + + /* to be overwritten in child classes */ + innerDestroy: function() { + + }, + + /* + Closes other containers except one related to passed element. + Other containers can be cancelled or submitted (depends on onblur option) + */ + closeOthers: function(element) { + $('.editable-open').each(function(i, el){ + //do nothing with passed element and it's children + if(el === element || $(el).find(element).length) { + return; + } + + //otherwise cancel or submit all open containers + var $el = $(el), + ec = $el.data('editableContainer'); + + if(!ec) { + return; + } + + if(ec.options.onblur === 'cancel') { + $el.data('editableContainer').hide('onblur'); + } else if(ec.options.onblur === 'submit') { + $el.data('editableContainer').tip().find('form').submit(); + } + }); + + }, + + /** + Activates input of visible container (e.g. set focus) + @method activate() + **/ + activate: function() { + if(this.tip && this.tip().is(':visible') && this.$form) { + this.$form.data('editableform').input.activate(); + } + } + + }; + + /** + jQuery method to initialize editableContainer. + + @method $().editableContainer(options) + @params {Object} options + @example + $('#edit').editableContainer({ + type: 'text', + url: '/post', + pk: 1, + value: 'hello' + }); + **/ + $.fn.editableContainer = function (option) { + var args = arguments; + return this.each(function () { + var $this = $(this), + dataKey = 'editableContainer', + data = $this.data(dataKey), + options = typeof option === 'object' && option, + Constructor = (options.mode === 'inline') ? Inline : Popup; + + if (!data) { + $this.data(dataKey, (data = new Constructor(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + //store constructors + $.fn.editableContainer.Popup = Popup; + $.fn.editableContainer.Inline = Inline; + + //defaults + $.fn.editableContainer.defaults = { + /** + Initial value of form input + + @property value + @type mixed + @default null + @private + **/ + value: null, + /** + Placement of container relative to element. Can be <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 only) + @property anim + @type string + @default false + **/ + anim: false, + + /** + Mode of editable, can be `popup` or `inline` + + @property mode + @type string + @default 'popup' + @since 1.4.0 + **/ + mode: 'popup' + }; + + /* + * workaround to have 'destroyed' event to destroy popover when element is destroyed + * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom + */ + jQuery.event.special.destroyed = { + remove: function(o) { + if (o.handler) { + o.handler(); + } + } + }; + +}(window.jQuery)); + +/** +* Editable Inline +* --------------------- +*/ +(function ($) { + "use strict"; + + //copy prototype from EditableContainer + //extend methods + $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { + containerName: 'editableform', + innerCss: '.editable-inline', + containerClass: 'editable-container editable-inline', //css class applied to container element + + initContainer: function(){ + //container is <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(); + } + + //check for transition support + if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { + this.options.highlight = false; + } + }; + + Editable.prototype = { + constructor: Editable, + init: function () { + var isValueByText = false, + doAutotext, finalize; + + //name + this.options.name = this.options.name || this.$element.attr('id'); + + //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select) + //also we set scope option to have access to element inside input specific callbacks (e. g. source as function) + this.options.scope = this.$element[0]; + this.input = $.fn.editableutils.createInput(this.options); + if(!this.input) { + return; + } + + //set value from settings or by element's text + if (this.options.value === undefined || this.options.value === null) { + this.value = this.input.html2value($.trim(this.$element.html())); + isValueByText = true; + } else { + /* + value can be string when received from 'data-value' attribute + for complext objects value can be set as json string in data-value attribute, + e.g. data-value="{city: 'Moscow', street: 'Lenina'}" + */ + this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); + if(typeof this.options.value === 'string') { + this.value = this.input.str2value(this.options.value); + } else { + this.value = this.options.value; + } + } + + //add 'editable' class to every editable element + this.$element.addClass('editable'); + + //attach handler activating editable. In disabled mode it just prevent default action (useful for links) + if(this.options.toggle !== 'manual') { + this.$element.addClass('editable-click'); + this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ + //prevent following link if editable enabled + if(!this.options.disabled) { + e.preventDefault(); + } + + //stop propagation not required because in document click handler it checks event target + //e.stopPropagation(); + + if(this.options.toggle === 'mouseenter') { + //for hover only show container + this.show(); + } else { + //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener + var closeAll = (this.options.toggle !== 'click'); + this.toggle(closeAll); + } + }, this)); + } else { + this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually + } + + //if display is function it's far more convinient to have autotext = always to render correctly on init + //see https://github.com/vitalets/x-editable-yii/issues/34 + if(typeof this.options.display === 'function') { + this.options.autotext = 'always'; + } + + //check conditions for autotext: + switch(this.options.autotext) { + case 'always': + doAutotext = true; + break; + case 'auto': + //if element text is empty and value is defined and value not generated by text --> run autotext + doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText; + break; + default: + doAutotext = false; + } + + //depending on autotext run render() or just finilize init + $.when(doAutotext ? this.render() : true).then($.proxy(function() { + if(this.options.disabled) { + this.disable(); + } else { + this.enable(); + } + /** + Fired when element was initialized by `$().editable()` method. + Please note that you should setup `init` handler **before** applying `editable`. + + @event init + @param {Object} event event object + @param {Object} editable editable instance (as here it cannot accessed via data('editable')) + @since 1.2.0 + @example + $('#username').on('init', function(e, editable) { + alert('initialized ' + editable.options.name); + }); + $('#username').editable(); + **/ + this.$element.triggerHandler('init', this); + }, this)); + }, + + /* + Initializes parent element for live editables + */ + initLive: function() { + //store selector + var selector = this.options.selector; + //modify options for child elements + this.options.selector = false; + this.options.autotext = 'never'; + //listen toggle events + this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ + var $target = $(e.target); + if(!$target.data('editable')) { + //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user) + //see https://github.com/vitalets/x-editable/issues/137 + if($target.hasClass(this.options.emptyclass)) { + $target.empty(); + } + $target.editable(this.options).trigger(e); + } + }, this)); + }, + + /* + Renders value into element's text. + Can call custom display method from options. + Can return deferred object. + @method render() + @param {mixed} response server response (if exist) to pass into display function + */ + render: function(response) { + //do not display anything + if(this.options.display === false) { + return; + } + + //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded + if(this.input.value2htmlFinal) { + return this.input.value2html(this.value, this.$element[0], this.options.display, response); + //if display method defined --> use it + } else if(typeof this.options.display === 'function') { + return this.options.display.call(this.$element[0], this.value, response); + //else use input's original value2html() method + } else { + return this.input.value2html(this.value, this.$element[0]); + } + }, + + /** + Enables editable + @method enable() + **/ + enable: function() { + this.options.disabled = false; + this.$element.removeClass('editable-disabled'); + this.handleEmpty(this.isEmpty); + if(this.options.toggle !== 'manual') { + if(this.$element.attr('tabindex') === '-1') { + this.$element.removeAttr('tabindex'); + } + } + }, + + /** + Disables editable + @method disable() + **/ + disable: function() { + this.options.disabled = true; + this.hide(); + this.$element.addClass('editable-disabled'); + this.handleEmpty(this.isEmpty); + //do not stop focus on this element + this.$element.attr('tabindex', -1); + }, + + /** + Toggles enabled / disabled state of editable element + @method toggleDisabled() + **/ + toggleDisabled: function() { + if(this.options.disabled) { + this.enable(); + } else { + this.disable(); + } + }, + + /** + Sets new option + + @method option(key, value) + @param {string|object} key option name or object with several options + @param {mixed} value option new value + @example + $('.editable').editable('option', 'pk', 2); + **/ + option: function(key, value) { + //set option(s) by object + if(key && typeof key === 'object') { + $.each(key, $.proxy(function(k, v){ + this.option($.trim(k), v); + }, this)); + return; + } + + //set option by string + this.options[key] = value; + + //disabled + if(key === 'disabled') { + return value ? this.disable() : this.enable(); + } + + //value + if(key === 'value') { + this.setValue(value); + } + + //transfer new option to container! + if(this.container) { + this.container.option(key, value); + } + + //pass option to input directly (as it points to the same in form) + if(this.input.option) { + this.input.option(key, value); + } + + }, + + /* + * set emptytext if element is empty + */ + handleEmpty: function (isEmpty) { + //do not handle empty if we do not display anything + if(this.options.display === false) { + return; + } + + /* + isEmpty may be set directly as param of method. + It is required when we enable/disable field and can't rely on content + as node content is text: "Empty" that is not empty %) + */ + if(isEmpty !== undefined) { + this.isEmpty = isEmpty; + } else { + //detect empty + if($.trim(this.$element.html()) === '') { + this.isEmpty = true; + } else if($.trim(this.$element.text()) !== '') { + this.isEmpty = false; + } else { + //e.g. '<img>' + this.isEmpty = !this.$element.height() || !this.$element.width(); + } + } + + //emptytext shown only for enabled + if(!this.options.disabled) { + if (this.isEmpty) { + this.$element.html(this.options.emptytext); + if(this.options.emptyclass) { + this.$element.addClass(this.options.emptyclass); + } + } else if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } + } else { + //below required if element disable property was changed + if(this.isEmpty) { + this.$element.empty(); + if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } + } + } + }, + + /** + Shows container with form + @method show() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + show: function (closeAll) { + if(this.options.disabled) { + return; + } + + //init editableContainer: popover, tooltip, inline, etc.. + if(!this.container) { + var containerOptions = $.extend({}, this.options, { + value: this.value, + input: this.input //pass input to form (as it is already created) + }); + this.$element.editableContainer(containerOptions); + //listen `save` event + this.$element.on("save.internal", $.proxy(this.save, this)); + this.container = this.$element.data('editableContainer'); + } else if(this.container.tip().is(':visible')) { + return; + } + + //show container + this.container.show(closeAll); + }, + + /** + Hides container with form + @method hide() + **/ + hide: function () { + if(this.container) { + this.container.hide(); + } + }, + + /** + Toggles container visibility (show / hide) + @method toggle() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + toggle: function(closeAll) { + if(this.container && this.container.tip().is(':visible')) { + this.hide(); + } else { + this.show(closeAll); + } + }, + + /* + * called when form was submitted + */ + save: function(e, params) { + //mark element with unsaved class if needed + if(this.options.unsavedclass) { + /* + Add unsaved css to element if: + - url is not user's function + - value was not sent to server + - params.response === undefined, that means data was not sent + - value changed + */ + var sent = false; + sent = sent || typeof this.options.url === 'function'; + sent = sent || this.options.display === false; + sent = sent || params.response !== undefined; + sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); + + if(sent) { + this.$element.removeClass(this.options.unsavedclass); + } else { + this.$element.addClass(this.options.unsavedclass); + } + } + + //highlight when saving + if(this.options.highlight) { + var $e = this.$element, + $bgColor = $e.css('background-color'); + + $e.css('background-color', this.options.highlight); + setTimeout(function(){ + $e.css('background-color', $bgColor); + $e.addClass('editable-bg-transition'); + setTimeout(function(){ + $e.removeClass('editable-bg-transition'); + }, 1700); + }, 0); + } + + //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(); + } + + this.input.destroy(); + + if(this.options.toggle !== 'manual') { + this.$element.removeClass('editable-click'); + this.$element.off(this.options.toggle + '.editable'); + } + + this.$element.off("save.internal"); + + this.$element.removeClass('editable editable-open editable-disabled'); + this.$element.removeData('editable'); + } + }; + + /* EDITABLE PLUGIN DEFINITION + * ======================= */ + + /** + jQuery method to initialize editable element. + + @method $().editable(options) + @params {Object} options + @example + $('#username').editable({ + type: 'text', + url: '/post', + pk: 1 + }); + **/ + $.fn.editable = function (option) { + //special API methods returning non-jquery object + var result = {}, args = arguments, datakey = 'editable'; + switch (option) { + /** + Runs client-side validation for all matched editables + + @method validate() + @returns {Object} validation errors map + @example + $('#username, #fullname').editable('validate'); + // possible result: + { + username: "username is required", + fullname: "fullname should be minimum 3 letters length" + } + **/ + case 'validate': + this.each(function () { + var $this = $(this), data = $this.data(datakey), error; + if (data && (error = data.validate())) { + result[data.options.name] = error; + } + }); + return result; + + /** + Returns current values of editable elements. + Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. + If value of some editable is `null` or `undefined` it is excluded from result object. + When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. + + @method getValue() + @param {bool} isSingle whether to return just value of single element + @returns {Object} object of element names and values + @example + $('#username, #fullname').editable('getValue'); + //result: + { + username: "superuser", + fullname: "John" + } + //isSingle = true + $('#username').editable('getValue', true); + //result "superuser" + **/ + case 'getValue': + if(arguments.length === 2 && arguments[1] === true) { //isSingle = true + result = this.eq(0).data(datakey).value; + } else { + this.each(function () { + var $this = $(this), data = $this.data(datakey); + if (data && data.value !== undefined && data.value !== null) { + result[data.options.name] = data.input.value2submit(data.value); + } + }); + } + return result; + + /** + This method collects values from several editable elements and submit them all to server. + Internally it runs client-side validation for all fields and submits only in case of success. + See <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, + /** + Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. + + @property highlight + @type string|boolean + @since 1.4.5 + @default #FFFF80 + **/ + highlight: '#FFFF80' + }; + +}(window.jQuery)); + +/** +AbstractInput - base class for all editable inputs. +It defines interface to be implemented by any input type. +To create your own input you can inherit from this class. + +@class abstractinput +**/ +(function ($) { + "use strict"; + + //types + $.fn.editabletypes = {}; + + var AbstractInput = function () { }; + + AbstractInput.prototype = { + /** + Initializes input + + @method init() + **/ + init: function(type, options, defaults) { + this.type = type; + this.options = $.extend({}, defaults, options); + }, + + /* + this method called before render to init $tpl that is inserted in DOM + */ + prerender: function() { + this.$tpl = $(this.options.tpl); //whole tpl as jquery object + this.$input = this.$tpl; //control itself, can be changed in render method + this.$clear = null; //clear button + this.error = null; //error message, if input cannot be rendered + }, + + /** + Renders input from tpl. Can return jQuery deferred object. + Can be overwritten in child objects + + @method render() + **/ + render: function() { + + }, + + /** + Sets element's html by value. + + @method value2html(value, element) + @param {mixed} value + @param {DOMElement} element + **/ + value2html: function(value, element) { + $(element).text($.trim(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() { + + }, + + /** + Additional actions when destroying element + **/ + destroy: function() { + }, + + // -------- helper functions -------- + setClass: function() { + if(this.options.inputclass) { + this.$input.addClass(this.options.inputclass); + } + }, + + setAttr: function(attr) { + if (this.options[attr] !== undefined && this.options[attr] !== null) { + this.$input.attr(attr, this.options[attr]); + } + }, + + option: function(key, value) { + this.options[key] = value; + } + + }; + + AbstractInput.defaults = { + /** + HTML template of input. Normally you should not change it. + + @property tpl + @type string + @default '' + **/ + tpl: '', + /** + CSS class automatically applied to input + + @property inputclass + @type string + @default 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) { + //run source if it function + var source; + if ($.isFunction(this.options.source)) { + source = this.options.source.call(this.options.scope); + this.sourceData = null; + //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed + } else { + source = this.options.source; + } + + //if allready loaded just call success + if(this.options.sourceCache && $.isArray(this.sourceData)) { + success.call(this); + return; + } + + //try parse json in single quotes (for double quotes jquery does automatically) + try { + source = $.fn.editableutils.tryParseJson(source, false); + } catch (e) { + error.call(this); + return; + } + + //loading from url + if (typeof source === 'string') { + //try to get sourceData from cache + if(this.options.sourceCache) { + var cacheID = source, + cache; + + if (!$(document).data(cacheID)) { + $(document).data(cacheID, {}); + } + cache = $(document).data(cacheID); + + //check for cached data + if (cache.loading === false && cache.sourceData) { //take source from cache + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + return; + } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later + cache.callbacks.push($.proxy(function () { + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + }, this)); + + //also collecting error callbacks + cache.err_callbacks.push($.proxy(error, this)); + return; + } else { //no cache yet, activate it + cache.loading = true; + cache.callbacks = []; + cache.err_callbacks = []; + } + } + + //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 select2 docs](http://ivaynberg.github.com/select2) for detailed description and options. +Compatible **select2 version is 3.4.1**! +You should manually download and include select2 distributive: + + <link href="select2/select2.css" rel="stylesheet" type="text/css"></link> + <script src="select2/select2.js"></script> + +To 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 `autotext` feature does not work for select2 with `ajax` remote source. +You need initially put both `data-value` and element's text youself: + + <a href="#" data-type="select2" data-value="1">Text1</a> + + +@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-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 || {}; + + this.sourceData = null; + + //placeholder + if(options.placeholder) { + options.select2.placeholder = options.placeholder; + } + + //if not `tags` mode, use source + if(!options.select2.tags && options.source) { + var source = options.source; + //if source is function, call it (once!) + if ($.isFunction(options.source)) { + source = options.source.call(options.scope); + } + + if (typeof source === 'string') { + options.select2.ajax = options.select2.ajax || {}; + //some default ajax params + if(!options.select2.ajax.data) { + options.select2.ajax.data = function(term) {return { query:term };}; + } + if(!options.select2.ajax.results) { + options.select2.ajax.results = function(data) { return {results:data };}; + } + options.select2.ajax.url = source; + } else { + //check format and convert x-editable format to select2 format (if needed) + this.sourceData = this.convertSource(source); + options.select2.data = this.sourceData; + } + } + + //overriding objects in config (as by default jQuery extend() is not recursive) + this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2); + + //detect whether it is multi-valued + this.isMultiple = this.options.select2.tags || this.options.select2.multiple; + this.isRemote = ('ajax' in this.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 to populate listData + if(this.isRemote) { + //listen to loaded event to populate data + this.$input.on('select2-loaded', $.proxy(function(e) { + this.sourceData = e.items.results; + }, this)); + } + + //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.options.select2.tags) { //in tags mode just assign value + data = value; + } else if(this.sourceData) { + data = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + } else { + //can not get list of possible values (e.g. autotext for select2 with ajax source) + } + + //data may be array (when multiple values allowed) + 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) { + //for remote source .val() is not working, need to look in sourceData + if(this.isRemote) { + //todo: check value for array + var item, items; + //if sourceData loaded, use it to get text for display + if(this.sourceData) { + items = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + if(items.length) { + item = items[0]; + } + } + //if item not found by sourceData, use element text (e.g. for the first show) + if(!item) { + item = {id: value, text: $(this.options.scope).text()}; + } + //select2('data', ...) allows to set both id and text --> usefull for initial show when items are not loaded + this.$input.select2('data', item).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + } else { + 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(); + } + }); + }, + + /* + Converts source from x-editable format: {value: 1, text: "1"} to + select2 format: {id: 1, text: "1"} + */ + convertSource: function(source) { + if($.isArray(source) && source.length && source[0].value !== undefined) { + for(var i = 0; i<source.length; i++) { + if(source[i].value !== undefined) { + source[i].id = source[i].value; + delete source[i].value; + } + } + } + return source; + } + + }); + + 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.4 +* 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 +* +* Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight +* In combodate: +* 12:00 pm --> 12:00 (24-h format, midday) +* 12:00 am --> 00:00 (24-h format, midnight, start of day) +* +* Differs from momentjs parse rules: +* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change) +* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change) +* +* +* 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) { + //see https://github.com/timrwood/momentjs.com/pull/36 + name = moment().date(1).month(i).format('MMMM'); + } else if(shortNames) { + name = moment().date(1).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, + min = h12 ? 1 : 0, + max = h12 ? 12 : 23; + + for(i=min; 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 from combos. + 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 12h --> 24h + if(this.$ampm) { + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour === 12) { + values.hour = this.$ampm.val() === 'am' ? 0 : 12; + } else { + values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; + } + } + + 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) { + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour >= 12) { + values.ampm = 'pm'; + if(values.hour > 12) { + values.hour -= 12; + } + } else { + values.ampm = 'am'; + if(values.hour === 0) { + values.hour = 12; + } + } + } + + $.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..00b61fc --- /dev/null +++ b/dist/jqueryui-editable/js/jqueryui-editable.min.js @@ -0,0 +1,5 @@ +/*! X-editable - v1.4.5 +* 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.isSaving=!1,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}var i=this.input.value2submit(r);this.isSaving=!0,e.when(this.save(i)).done(e.proxy(function(e){this.isSaving=!1;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,submitValue:i,response:e})},this)).fail(e.proxy(function(e){this.isSaving=!1;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){this.options.pk=e.fn.editableutils.tryParseJson(this.options.pk,!0);var n=typeof this.options.pk=="function"?this.options.pk.call(this.options.scope):this.options.pk,r=!!(typeof this.options.url=="function"||this.options.url&&(this.options.send==="always"||this.options.send==="auto"&&n!==null&&n!==undefined)),i;if(r)return this.showLoading(),i={name:this.options.name||"",value:t,pk:n},typeof this.options.params=="function"?i=this.options.params.call(this.options.scope,i):(this.options.params=e.fn.editableutils.tryParseJson(this.options.params,!0),e.extend(i,this.options.params)),typeof this.options.url=="function"?this.options.url.call(this.options.scope,i):e.ajax(e.extend({url:this.options.url,data:i,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)},supportsTransitions:function(){var e=document.body||document.documentElement,t=e.style,n="transition",r=["Moz","Webkit","Khtml","O","ms"];if(typeof t[n]=="string")return!0;n=n.charAt(0).toUpperCase()+n.substr(1);for(var i=0;i<r.length;i++)if(typeof t[r[i]+n]=="string")return!0;return!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.delayedHide=!1,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(function(){this.delayedHide?(this.hide(this.delayedHide.reason),this.delayedHide=!1):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;if(this.$form.data("editableform").isSaving){this.delayedHide={reason:e};return}this.delayedHide=!1,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(),this.options.highlight&&!e.fn.editableutils.supportsTransitions()&&(this.options.highlight=!1)};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){this.options.disabled||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),typeof this.options.display=="function"&&(this.options.autotext="always");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;t!==undefined?this.isEmpty=t:e.trim(this.$element.html())===""?this.isEmpty=!0:e.trim(this.$element.text())!==""?this.isEmpty=!1:this.isEmpty=!this.$element.height()||!this.$element.width(),this.options.disabled?this.isEmpty&&(this.$element.empty(),this.options.emptyclass&&this.$element.removeClass(this.options.emptyclass)):this.isEmpty?(this.$element.html(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)}if(this.options.highlight){var r=this.$element,i=r.css("background-color");r.css("background-color",this.options.highlight),setTimeout(function(){r.css("background-color",i),r.addClass("editable-bg-transition"),setTimeout(function(){r.removeClass("editable-bg-transition")},1700)},0)}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.input.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 arguments.length===2&&arguments[1]===!0?r=this.eq(0).data(s).value: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,highlight:"#FFFF80"}}(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(e.trim(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(){},destroy: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){var r;e.isFunction(this.options.source)?(r=this.options.source.call(this.options.scope),this.sourceData=null):r=this.options.source;if(this.options.sourceCache&&e.isArray(this.sourceData)){t.call(this);return}try{r=e.fn.editableutils.tryParseJson(r,!1)}catch(i){n.call(this);return}if(typeof r=="string"){if(this.options.sourceCache){var s=r,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:r,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(r),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||{},this.sourceData=null,n.placeholder&&(n.select2.placeholder=n.placeholder);if(!n.select2.tags&&n.source){var r=n.source;e.isFunction(n.source)&&(r=n.source.call(n.scope)),typeof r=="string"?(n.select2.ajax=n.select2.ajax||{},n.select2.ajax.data||(n.select2.ajax.data=function(e){return{query:e}}),n.select2.ajax.results||(n.select2.ajax.results=function(e){return{results:e}}),n.select2.ajax.url=r):(this.sourceData=this.convertSource(r),n.select2.data=this.sourceData)}this.options.select2=e.extend({},t.defaults.select2,n.select2),this.isMultiple=this.options.select2.tags||this.options.select2.multiple,this.isRemote="ajax"in this.options.select2};e.fn.editableutils.inherit(t,e.fn.editabletypes.abstractinput),e.extend(t.prototype,{render:function(){this.setClass(),this.$input.select2(this.options.select2),this.isRemote&&this.$input.on("select2-loaded",e.proxy(function(e){this.sourceData=e.items.results},this)),this.isMultiple&&this.$input.on("change",function(){e(this).closest("form").parent().triggerHandler("resize")})},value2html:function(t,n){var r="",i;this.options.select2.tags?i=t:this.sourceData&&(i=e.fn.editableutils.itemsByValue(t,this.sourceData,"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(t){if(this.isRemote){var n,r;this.sourceData&&(r=e.fn.editableutils.itemsByValue(t,this.sourceData,"id"),r.length&&(n=r[0])),n||(n={id:t,text:e(this.options.scope).text()}),this.$input.select2("data",n).trigger("change",!0)}else this.$input.val(t).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()})},convertSource:function(t){if(e.isArray(t)&&t.length&&t[0].value!==undefined)for(var n=0;n<t.length;n++)t[n].value!==undefined&&(t[n].id=t[n].value,delete t[n].value);return t}}),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().date(1).month(n).format("MMMM"):i?t=moment().date(1).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?1:0,u=r?12:23;for(n=o;n<=u;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===12?r.hour=this.$ampm.val()==="am"?0:12:r.hour=this.$ampm.val()==="am"?r.hour:r.hour+12),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.ampm="pm",i.hour>12&&(i.hour-=12)):(i.ampm="am",i.hour===0&&(i.hour=12))),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 diff --git a/grunt.js b/grunt.js index f754c23..cdf5806 100644 --- a/grunt.js +++ b/grunt.js @@ -12,9 +12,9 @@ function getFiles() { form: [forms+'editable-form-bootstrap.js'], container: [containers+'editable-popover.js'], inputs: [ + inputs+'date/bootstrap-datepicker/js/bootstrap-datepicker.js', inputs+'date/date.js', inputs+'date/datefield.js', - inputs+'date/bootstrap-datepicker/js/bootstrap-datepicker.js', inputs+'datetime/datetime.js', inputs+'datetime/datetimefield.js', //don't build datetime lib, should be included manually diff --git a/package.json b/package.json index c09f9ee..fa9d9e6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "X-editable", "title": "X-editable", "description": "In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery", - "version": "1.4.4", + "version": "1.4.5", "homepage": "http://github.com/vitalets/x-editable", "author": { "name": "Vitaliy Potapov", diff --git a/src/containers/editable-container.js b/src/containers/editable-container.js index 37ea578..17d4e64 100644 --- a/src/containers/editable-container.js +++ b/src/containers/editable-container.js @@ -33,6 +33,9 @@ Applied as jQuery method. this.formOptions.scope = this.$element[0]; this.initContainer(); + + //flag to hide container, when saving value will finish + this.delayedHide = false; //bind 'destroyed' listener to destroy container when element is removed from dom this.$element.on('destroyed', $.proxy(function(){ @@ -137,7 +140,14 @@ Applied as jQuery method. 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) + show: $.proxy(function() { + if(this.delayedHide) { + this.hide(this.delayedHide.reason); + this.delayedHide = false; + } else { + 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(){ @@ -181,11 +191,11 @@ Applied as jQuery method. /* 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(). + The main reason is that we dont know, what will container do with content when closed: + remove(), detach() or just hide() - it depends on container. Detaching form itself before hide and re-insert before show is good solution, - but visually it looks ugly, as container changes size before hide. + but visually it looks ugly --> container changes size before hide. */ //if form already exist - delete previous data @@ -218,10 +228,18 @@ Applied as jQuery method. return; } + //if form is saving value, schedule hide + if(this.$form.data('editableform').isSaving) { + this.delayedHide = {reason: reason}; + return; + } else { + this.delayedHide = false; + } + this.$element.removeClass('editable-open'); this.innerHide(); - - /** + + /** Fired when container was hidden. It occurs on both save or cancel. **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. The workaround is to check `arguments.length` that is always `2` for x-editable. @@ -235,20 +253,20 @@ Applied as jQuery method. //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) @@ -293,7 +311,7 @@ Applied as jQuery method. **/ this.$element.triggerHandler('save', params); - //hide must be after trigger, as saving value may require methods od plugin, applied to input + //hide must be after trigger, as saving value may require methods of plugin, applied to input this.hide('save'); }, @@ -453,7 +471,7 @@ Applied as jQuery method. onblur: 'cancel', /** - Animation speed (inline mode) + Animation speed (inline mode only) @property anim @type string @default false diff --git a/src/containers/editable-popover.js b/src/containers/editable-popover.js index d5c7297..e520f6f 100644 --- a/src/containers/editable-popover.js +++ b/src/containers/editable-popover.js @@ -59,7 +59,7 @@ */ /*jshint laxcomma: true*/ setPosition: function () { - + (function() { var $tip = this.tip() , inside diff --git a/src/containers/editable-poshytip.js b/src/containers/editable-poshytip.js index 0a67f88..b797080 100644 --- a/src/containers/editable-poshytip.js +++ b/src/containers/editable-poshytip.js @@ -43,7 +43,7 @@ this.call('update', $content); this.call('show'); - this.tip().addClass('editable-container'); + this.tip().addClass(this.containerClass); this.$form.data('editableform').input.activate(); }, diff --git a/src/editable-form/editable-form-utils.js b/src/editable-form/editable-form-utils.js index 304e050..2c9457d 100644 --- a/src/editable-form/editable-form-utils.js +++ b/src/editable-form/editable-form-utils.js @@ -218,6 +218,27 @@ $.error('Unknown type: '+ type); return false; } + }, + + //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr + supportsTransitions: function () { + var b = document.body || document.documentElement, + s = b.style, + p = 'transition', + v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms']; + + if(typeof s[p] === 'string') { + return true; + } + + // Tests for vendor specific prop + p = p.charAt(0).toUpperCase() + p.substr(1); + for(var i=0; i<v.length; i++) { + if(typeof s[v[i] + p] === 'string') { + return true; + } + } + return false; } }; diff --git a/src/editable-form/editable-form.js b/src/editable-form/editable-form.js index eb27e40..602be5f 100644 --- a/src/editable-form/editable-form.js +++ b/src/editable-form/editable-form.js @@ -60,6 +60,10 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. //show loading state this.showLoading(); + //flag showing is form now saving value to server. + //It is needed to wait when closing form. + this.isSaving = false; + /** Fired when rendering starts @event rendering @@ -212,31 +216,38 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. return; } + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + this.isSaving = true; + //sending data to server - $.when(this.save(newValue)) + $.when(this.save(submitValue)) .done($.proxy(function(response) { + this.isSaving = false; + //run success callback var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; - + //if success callback returns false --> keep form open and do not activate input if(res === false) { this.error(false); this.showForm(false); return; - } - + } + //if success callback returns string --> keep form open, show error and activate input if(typeof res === 'string') { this.error(res); this.showForm(); return; - } - + } + //if success callback returns object like {newValue: <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); @@ -246,37 +257,42 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc. @event save @param {Object} event event object @param {Object} params additional params - @param {mixed} params.newValue submitted value + @param {mixed} params.newValue raw new value + @param {mixed} params.submitValue submitted value as string @param {Object} params.response ajax response @example $('#form-div').on('save'), function(e, params){ if(params.newValue === 'username') {...} - }); - **/ - this.$div.triggerHandler('save', {newValue: newValue, response: response}); + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response}); }, this)) .fail($.proxy(function(xhr) { + this.isSaving = false; + var msg; if(typeof this.options.error === 'function') { msg = this.options.error.call(this.options.scope, xhr, newValue); } else { msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; } - + this.error(msg); this.showForm(); }, this)); }, - save: function(newValue) { - //convert value for submitting to server - var submitValue = this.input.value2submit(newValue); - + save: function(submitValue) { //try parse composite pk defined as json string in data-pk this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, + /* + send on server in following cases: + 1. url is function + 2. url is string AND (pk defined OR send option = always) + */ send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), params; diff --git a/src/element/editable-element.css b/src/element/editable-element.css index ddb92ae..b2943f8 100644 --- a/src/element/editable-element.css +++ b/src/element/editable-element.css @@ -28,6 +28,14 @@ a.editable-click.editable-disabled:hover { /* content: '*'*/ } +.editable-bg-transition { + -webkit-transition: background-color 1400ms ease-out; + -moz-transition: background-color 1400ms ease-out; + -o-transition: background-color 1400ms ease-out; + -ms-transition: background-color 1400ms ease-out; + transition: background-color 1400ms ease-out; +} + /*see https://github.com/vitalets/x-editable/issues/139 */ .form-horizontal .editable { diff --git a/src/element/editable-element.js b/src/element/editable-element.js index 69be05d..8550c1f 100644 --- a/src/element/editable-element.js +++ b/src/element/editable-element.js @@ -16,6 +16,11 @@ Makes editable any HTML element on the page. Applied as jQuery method. } else { this.init(); } + + //check for transition support + if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) { + this.options.highlight = false; + } }; Editable.prototype = { @@ -60,25 +65,33 @@ Makes editable any HTML element on the page. Applied as jQuery method. 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(); + //prevent following link if editable enabled + if(!this.options.disabled) { + e.preventDefault(); + } //stop propagation not required because in document click handler it checks event target //e.stopPropagation(); if(this.options.toggle === 'mouseenter') { //for hover only show container - this.show(); + this.show(); } else { //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener var closeAll = (this.options.toggle !== 'click'); this.toggle(closeAll); - } + } }, this)); } else { this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually } + //if display is function it's far more convinient to have autotext = always to render correctly on init + //see https://github.com/vitalets/x-editable-yii/issues/34 + if(typeof this.options.display === 'function') { + this.options.autotext = 'always'; + } + //check conditions for autotext: switch(this.options.autotext) { case 'always': @@ -257,12 +270,29 @@ Makes editable any HTML element on the page. Applied as jQuery method. return; } - this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === ''; + /* + isEmpty may be set directly as param of method. + It is required when we enable/disable field and can't rely on content + as node content is text: "Empty" that is not empty %) + */ + if(isEmpty !== undefined) { + this.isEmpty = isEmpty; + } else { + //detect empty + if($.trim(this.$element.html()) === '') { + this.isEmpty = true; + } else if($.trim(this.$element.text()) !== '') { + this.isEmpty = false; + } else { + //e.g. '<img>' + this.isEmpty = !this.$element.height() || !this.$element.width(); + } + } //emptytext shown only for enabled if(!this.options.disabled) { if (this.isEmpty) { - this.$element.text(this.options.emptytext); + this.$element.html(this.options.emptytext); if(this.options.emptyclass) { this.$element.addClass(this.options.emptyclass); } @@ -357,6 +387,21 @@ Makes editable any HTML element on the page. Applied as jQuery method. } } + //highlight when saving + if(this.options.highlight) { + var $e = this.$element, + $bgColor = $e.css('background-color'); + + $e.css('background-color', this.options.highlight); + setTimeout(function(){ + $e.css('background-color', $bgColor); + $e.addClass('editable-bg-transition'); + setTimeout(function(){ + $e.removeClass('editable-bg-transition'); + }, 1700); + }, 0); + } + //set new value this.setValue(params.newValue, false, params.response); @@ -423,6 +468,8 @@ Makes editable any HTML element on the page. Applied as jQuery method. if(this.container) { this.container.destroy(); } + + this.input.destroy(); if(this.options.toggle !== 'manual') { this.$element.removeClass('editable-click'); @@ -480,28 +527,37 @@ Makes editable any HTML element on the page. Applied as jQuery method. /** 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. + If value of some editable is `null` or `undefined` it is excluded from result object. + When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object. @method getValue() + @param {bool} isSingle whether to return just value of single element @returns {Object} object of element names and values @example $('#username, #fullname').editable('getValue'); - // possible result: + //result: { username: "superuser", fullname: "John" } + //isSingle = true + $('#username').editable('getValue', true); + //result "superuser" **/ 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); - } - }); + if(arguments.length === 2 && arguments[1] === true) { //isSingle = true + result = this.eq(0).data(datakey).value; + } else { + this.each(function () { + var $this = $(this), data = $this.data(datakey); + if (data && data.value !== undefined && data.value !== null) { + result[data.options.name] = data.input.value2submit(data.value); + } + }); + } return result; - /** + /** This method collects values from several editable elements and submit them all to server. Internally it runs client-side validation for all fields and submits only in case of success. See <a href="#newrecord">creating new records</a> for details. @@ -725,7 +781,16 @@ Makes editable any HTML element on the page. Applied as jQuery method. }); </script> **/ - selector: null + selector: null, + /** + Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. + + @property highlight + @type string|boolean + @since 1.4.5 + @default #FFFF80 + **/ + highlight: '#FFFF80' }; }(window.jQuery)); diff --git a/src/inputs/abstract.js b/src/inputs/abstract.js index b037b0b..547ba10 100644 --- a/src/inputs/abstract.js +++ b/src/inputs/abstract.js @@ -7,23 +7,23 @@ To create your own input you can inherit from this class. **/ (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 */ @@ -37,107 +37,107 @@ To create your own input you can inherit from this class. /** Renders input from tpl. Can return jQuery deferred object. Can be overwritten in child objects - - @method render() - **/ + + @method render() + **/ render: function() { }, /** Sets element's html by value. - - @method value2html(value, element) + + @method value2html(value, element) @param {mixed} value @param {DOMElement} element - **/ + **/ value2html: function(value, element) { - $(element).text(value); + $(element).text($.trim(value)); }, - + /** Converts element's html to value - - @method html2value(html) + + @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) + + @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. **/ @@ -147,18 +147,24 @@ To create your own input you can inherit from this class. /** attach handler to automatically submit form when value changed (useful when buttons not shown) - **/ + **/ autosubmit: function() { }, + /** + Additional actions when destroying element + **/ + destroy: function() { + }, + // -------- helper functions -------- setClass: function() { if(this.options.inputclass) { this.$input.addClass(this.options.inputclass); } }, - + setAttr: function(attr) { if (this.options[attr] !== undefined && this.options[attr] !== null) { this.$input.attr(attr, this.options[attr]); diff --git a/src/inputs/combodate/lib/combodate.js b/src/inputs/combodate/lib/combodate.js index 3313064..1f38737 100644 --- a/src/inputs/combodate/lib/combodate.js +++ b/src/inputs/combodate/lib/combodate.js @@ -1,10 +1,20 @@ /** -* Combodate - 1.0.3 +* Combodate - 1.0.4 * 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 * +* Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight +* In combodate: +* 12:00 pm --> 12:00 (24-h format, midday) +* 12:00 am --> 00:00 (24-h format, midnight, start of day) +* +* Differs from momentjs parse rules: +* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change) +* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change) +* +* * Author: Vitaliy Potapov * Project page: http://github.com/vitalets/combodate * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License. @@ -154,9 +164,10 @@ for(i=0; i<=11; i++) { if(longNames) { - name = moment().month(i).format('MMMM'); + //see https://github.com/timrwood/momentjs.com/pull/36 + name = moment().date(1).month(i).format('MMMM'); } else if(shortNames) { - name = moment().month(i).format('MMM'); + name = moment().date(1).month(i).format('MMM'); } else if(twoDigit) { name = this.leadZero(i+1); } else { @@ -192,9 +203,10 @@ h12 = this.options.template.indexOf('h') !== -1, h24 = this.options.template.indexOf('H') !== -1, twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1, + min = h12 ? 1 : 0, max = h12 ? 12 : 23; - for(i=0; i<=max; i++) { + for(i=min; i<=max; i++) { name = twoDigit ? this.leadZero(i) : i; items.push([i, name]); } @@ -243,7 +255,7 @@ }, /* - Returns current date value. + Returns current date value from combos. If format not specified - `options.format` used. If format = `null` - Moment object returned. */ @@ -272,12 +284,14 @@ return ''; } - //convert hours if 12h format + //convert hours 12h --> 24h if(this.$ampm) { - values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; - if(values.hour === 24) { - values.hour = 0; - } + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour === 12) { + values.hour = this.$ampm.val() === 'am' ? 0 : 12; + } else { + values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12; + } } dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]); @@ -328,11 +342,17 @@ }); if(this.$ampm) { - if(values.hour > 12) { - values.hour -= 12; + //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day) + if(values.hour >= 12) { values.ampm = 'pm'; + if(values.hour > 12) { + values.hour -= 12; + } } else { - values.ampm = 'am'; + values.ampm = 'am'; + if(values.hour === 0) { + values.hour = 12; + } } } diff --git a/src/inputs/date/bootstrap-datepicker/css/datepicker.css b/src/inputs/date/bootstrap-datepicker/css/datepicker.css index 5ecec5d..6b7ff18 100644 --- a/src/inputs/date/bootstrap-datepicker/css/datepicker.css +++ b/src/inputs/date/bootstrap-datepicker/css/datepicker.css @@ -111,7 +111,7 @@ 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; + color: #000; } .datepicker table tr td.today:hover, .datepicker table tr td.today:hover:hover, @@ -145,6 +145,124 @@ .datepicker table tr td.today.disabled:hover.active { background-color: #fbf069 \9; } +.datepicker table tr td.today:hover:hover { + color: #000; +} +.datepicker table tr td.today.active:hover { + color: #fff; +} +.datepicker table tr td.range, +.datepicker table tr td.range:hover, +.datepicker table tr td.range.disabled, +.datepicker table tr td.range.disabled:hover { + background: #eeeeee; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.datepicker table tr td.range.today, +.datepicker table tr td.range.today:hover, +.datepicker table tr td.range.today.disabled, +.datepicker table tr td.range.today.disabled:hover { + background-color: #f3d17a; + background-image: -moz-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -ms-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a)); + background-image: -webkit-linear-gradient(top, #f3c17a, #f3e97a); + background-image: -o-linear-gradient(top, #f3c17a, #f3e97a); + background-image: linear-gradient(top, #f3c17a, #f3e97a); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0); + border-color: #f3e97a #f3e97a #edde34; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.datepicker table tr td.range.today:hover, +.datepicker table tr td.range.today:hover:hover, +.datepicker table tr td.range.today.disabled:hover, +.datepicker table tr td.range.today.disabled:hover:hover, +.datepicker table tr td.range.today:active, +.datepicker table tr td.range.today:hover:active, +.datepicker table tr td.range.today.disabled:active, +.datepicker table tr td.range.today.disabled:hover:active, +.datepicker table tr td.range.today.active, +.datepicker table tr td.range.today:hover.active, +.datepicker table tr td.range.today.disabled.active, +.datepicker table tr td.range.today.disabled:hover.active, +.datepicker table tr td.range.today.disabled, +.datepicker table tr td.range.today:hover.disabled, +.datepicker table tr td.range.today.disabled.disabled, +.datepicker table tr td.range.today.disabled:hover.disabled, +.datepicker table tr td.range.today[disabled], +.datepicker table tr td.range.today:hover[disabled], +.datepicker table tr td.range.today.disabled[disabled], +.datepicker table tr td.range.today.disabled:hover[disabled] { + background-color: #f3e97a; +} +.datepicker table tr td.range.today:active, +.datepicker table tr td.range.today:hover:active, +.datepicker table tr td.range.today.disabled:active, +.datepicker table tr td.range.today.disabled:hover:active, +.datepicker table tr td.range.today.active, +.datepicker table tr td.range.today:hover.active, +.datepicker table tr td.range.today.disabled.active, +.datepicker table tr td.range.today.disabled:hover.active { + background-color: #efe24b \9; +} +.datepicker table tr td.selected, +.datepicker table tr td.selected:hover, +.datepicker table tr td.selected.disabled, +.datepicker table tr td.selected.disabled:hover { + background-color: #9e9e9e; + background-image: -moz-linear-gradient(top, #b3b3b3, #808080); + background-image: -ms-linear-gradient(top, #b3b3b3, #808080); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080)); + background-image: -webkit-linear-gradient(top, #b3b3b3, #808080); + background-image: -o-linear-gradient(top, #b3b3b3, #808080); + background-image: linear-gradient(top, #b3b3b3, #808080); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0); + border-color: #808080 #808080 #595959; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td.selected:hover, +.datepicker table tr td.selected:hover:hover, +.datepicker table tr td.selected.disabled:hover, +.datepicker table tr td.selected.disabled:hover:hover, +.datepicker table tr td.selected:active, +.datepicker table tr td.selected:hover:active, +.datepicker table tr td.selected.disabled:active, +.datepicker table tr td.selected.disabled:hover:active, +.datepicker table tr td.selected.active, +.datepicker table tr td.selected:hover.active, +.datepicker table tr td.selected.disabled.active, +.datepicker table tr td.selected.disabled:hover.active, +.datepicker table tr td.selected.disabled, +.datepicker table tr td.selected:hover.disabled, +.datepicker table tr td.selected.disabled.disabled, +.datepicker table tr td.selected.disabled:hover.disabled, +.datepicker table tr td.selected[disabled], +.datepicker table tr td.selected:hover[disabled], +.datepicker table tr td.selected.disabled[disabled], +.datepicker table tr td.selected.disabled:hover[disabled] { + background-color: #808080; +} +.datepicker table tr td.selected:active, +.datepicker table tr td.selected:hover:active, +.datepicker table tr td.selected.disabled:active, +.datepicker table tr td.selected.disabled:hover:active, +.datepicker table tr td.selected.active, +.datepicker table tr td.selected:hover.active, +.datepicker table tr td.selected.disabled.active, +.datepicker table tr td.selected.disabled:hover.active { + background-color: #666666 \9; +} .datepicker table tr td.active, .datepicker table tr td.active:hover, .datepicker table tr td.active.disabled, @@ -268,18 +386,19 @@ .datepicker table tr td span.active.disabled:hover.active { background-color: #003399 \9; } -.datepicker table tr td span.old { +.datepicker table tr td span.old, +.datepicker table tr td span.new { color: #999999; } -.datepicker th.switch { +.datepicker th.datepicker-switch { width: 145px; } .datepicker thead tr:first-child th, -.datepicker tfoot tr:first-child th { +.datepicker tfoot tr th { cursor: pointer; } .datepicker thead tr:first-child th:hover, -.datepicker tfoot tr:first-child th:hover { +.datepicker tfoot tr th:hover { background: #eeeeee; } .datepicker .cw { @@ -299,3 +418,32 @@ width: 16px; height: 16px; } +.input-daterange input { + text-align: center; +} +.input-daterange input:first-child { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-daterange input:last-child { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-daterange .add-on { + display: inline-block; + width: auto; + min-width: 16px; + height: 18px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + vertical-align: middle; + background-color: #eeeeee; + border: 1px solid #ccc; + margin-left: -5px; + margin-right: -5px; +} diff --git a/src/inputs/date/bootstrap-datepicker/js/bootstrap-datepicker.js b/src/inputs/date/bootstrap-datepicker/js/bootstrap-datepicker.js index 8991aa8..b644a70 100644 --- a/src/inputs/date/bootstrap-datepicker/js/bootstrap-datepicker.js +++ b/src/inputs/date/bootstrap-datepicker/js/bootstrap-datepicker.js @@ -18,7 +18,7 @@ * limitations under the License. * ========================================================= */ -!function( $ ) { +(function( $ ) { function UTCDate(){ return new Date(Date.UTC.apply(Date, arguments)); @@ -33,12 +33,9 @@ var Datepicker = function(element, options) { var that = this; + this._process_options(options); + 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; @@ -46,13 +43,6 @@ 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(); @@ -62,65 +52,17 @@ } else { this.picker.addClass('datepicker-dropdown dropdown-menu'); } - if (this.isRTL){ + + if (this.o.rtl){ 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.o.startView; - 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) + if (this.o.calendarWeeks) this.picker.find('tfoot th.today') .attr('colspan', function(i, val){ return parseInt(val) + 1; @@ -128,14 +70,10 @@ 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.setStartDate(this.o.startDate); + this.setEndDate(this.o.endDate); + this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled); + this.fillDow(); this.fillMonths(); @@ -152,6 +90,68 @@ Datepicker.prototype = { constructor: Datepicker, + _process_options: function(opts){ + // Store raw options for reference + this._o = $.extend({}, this._o, opts); + // Processed options + var o = this.o = $.extend({}, this._o); + + // Check if "de-DE" style date is available, if not language should + // fallback to 2 letter code eg "de" + var lang = o.language; + if (!dates[lang]) { + lang = lang.split('-')[0]; + if (!dates[lang]) + lang = defaults.language; + } + o.language = lang; + + switch(o.startView){ + case 2: + case 'decade': + o.startView = 2; + break; + case 1: + case 'year': + o.startView = 1; + break; + default: + o.startView = 0; + } + + switch (o.minViewMode) { + case 1: + case 'months': + o.minViewMode = 1; + break; + case 2: + case 'years': + o.minViewMode = 2; + break; + default: + o.minViewMode = 0; + } + + o.startView = Math.max(o.startView, o.minViewMode); + + o.weekStart %= 7; + o.weekEnd = ((o.weekStart + 6) % 7); + + var format = DPGlobal.parseFormat(o.format) + if (o.startDate !== -Infinity) { + o.startDate = DPGlobal.parseDate(o.startDate, format, o.language); + } + if (o.endDate !== Infinity) { + o.endDate = DPGlobal.parseDate(o.endDate, format, o.language); + } + + o.daysOfWeekDisabled = o.daysOfWeekDisabled||[]; + if (!$.isArray(o.daysOfWeekDisabled)) + o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/); + o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function (d) { + return parseInt(d, 10); + }); + }, _events: [], _secondaryEvents: [], _applyEvents: function(evs){ @@ -212,7 +212,12 @@ [$(document), { mousedown: $.proxy(function (e) { // Clicked outside the datepicker, hide it - if ($(e.target).closest('.datepicker.datepicker-inline, .datepicker.datepicker-dropdown').length === 0) { + if (!( + this.element.is(e.target) || + this.element.find(e.target).size() || + this.picker.is(e.target) || + this.picker.find(e.target).size() + )) { this.hide(); } }, this) @@ -233,6 +238,19 @@ _detachSecondaryEvents: function(){ this._unapplyEvents(this._secondaryEvents); }, + _trigger: function(event, altdate){ + var date = altdate || this.date, + local_date = new Date(date.getTime() + (date.getTimezoneOffset()*60000)); + + this.element.trigger({ + type: event, + date: local_date, + format: $.proxy(function(altformat){ + var format = altformat || this.o.format; + return DPGlobal.formatDate(date, format, this.o.language); + }, this) + }); + }, show: function(e) { if (!this.isInline) @@ -244,10 +262,7 @@ if (e) { e.preventDefault(); } - this.element.trigger({ - type: 'show', - date: this.date - }); + this._trigger('show'); }, hide: function(e){ @@ -255,21 +270,18 @@ if (!this.picker.is(':visible')) return; this.picker.hide().detach(); this._detachSecondaryEvents(); - this.viewMode = this.startViewMode; + this.viewMode = this.o.startView; this.showMode(); if ( - this.forceParse && + this.o.forceParse && ( this.isInput && this.element.val() || this.hasInput && this.element.find('input').val() ) ) this.setValue(); - this.element.trigger({ - type: 'hide', - date: this.date - }); + this._trigger('hide'); }, remove: function() { @@ -307,7 +319,6 @@ if (this.component){ this.element.find('input').val(formatted); } - this.element.data('date', formatted); } else { this.element.val(formatted); } @@ -315,36 +326,24 @@ getFormattedDate: function(format) { if (format === undefined) - format = this.format; - return DPGlobal.formatDate(this.date, format, this.language); + format = this.o.format; + return DPGlobal.formatDate(this.date, format, this.o.language); }, setStartDate: function(startDate){ - this.startDate = startDate||-Infinity; - if (this.startDate !== -Infinity) { - this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language); - } + this._process_options({startDate: startDate}); 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._process_options({endDate: endDate}); 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._process_options({daysOfWeekDisabled: daysOfWeekDisabled}); this.update(); this.updateNavArrows(); }, @@ -373,16 +372,17 @@ fromArgs = true; } else { date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val(); + delete this.element.data().date; } - this.date = DPGlobal.parseDate(date, this.format, this.language); + this.date = DPGlobal.parseDate(date, this.o.format, this.o.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); + if (this.date < this.o.startDate) { + this.viewDate = new Date(this.o.startDate); + } else if (this.date > this.o.endDate) { + this.viewDate = new Date(this.o.endDate); } else { this.viewDate = new Date(this.date); } @@ -390,15 +390,15 @@ }, fillDow: function(){ - var dowCnt = this.weekStart, + var dowCnt = this.o.weekStart, html = '<tr>'; - if(this.calendarWeeks){ + if(this.o.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>'; + while (dowCnt < this.o.weekStart + 7) { + html += '<th class="dow">'+dates[this.o.language].daysMin[(dowCnt++)%7]+'</th>'; } html += '</tr>'; this.picker.find('.datepicker-days thead').append(html); @@ -408,46 +408,93 @@ var html = '', i = 0; while (i < 12) { - html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>'; + html += '<span class="month">'+dates[this.o.language].monthsShort[i++]+'</span>'; } this.picker.find('.datepicker-months td').html(html); }, + setRange: function(range){ + if (!range || !range.length) + delete this.range; + else + this.range = $.map(range, function(d){ return d.valueOf(); }); + this.fill(); + }, + + getClassNames: function(date){ + var cls = [], + year = this.viewDate.getUTCFullYear(), + month = this.viewDate.getUTCMonth(), + currentDate = this.date.valueOf(), + today = new Date(); + if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) { + cls.push('old'); + } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) { + cls.push('new'); + } + // Compare internal UTC date with local today, not UTC today + if (this.o.todayHighlight && + date.getUTCFullYear() == today.getFullYear() && + date.getUTCMonth() == today.getMonth() && + date.getUTCDate() == today.getDate()) { + cls.push('today'); + } + if (currentDate && date.valueOf() == currentDate) { + cls.push('active'); + } + if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate || + $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) { + cls.push('disabled'); + } + if (this.range){ + if (date > this.range[0] && date < this.range[this.range.length-1]){ + cls.push('range'); + } + if ($.inArray(date.valueOf(), this.range) != -1){ + cls.push('selected'); + } + } + return cls; + }, + 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, + startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, + startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, + endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, + endMonth = this.o.endDate !== Infinity ? this.o.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); + tooltip; + this.picker.find('.datepicker-days thead th.datepicker-switch') + .text(dates[this.o.language].months[month]+' '+year); this.picker.find('tfoot th.today') - .text(dates[this.language].today) - .toggle(this.todayBtn !== false); + .text(dates[this.o.language].today) + .toggle(this.o.todayBtn !== false); + this.picker.find('tfoot th.clear') + .text(dates[this.o.language].clear) + .toggle(this.o.clearBtn !== 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); + prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.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) { + if (prevMonth.getUTCDay() == this.o.weekStart) { html.push('<tr>'); - if(this.calendarWeeks){ + if(this.o.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), + ws = new Date(+prevMonth + (this.o.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 @@ -458,28 +505,26 @@ } } - 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) { + clsName = this.getClassNames(prevMonth); + clsName.push('day'); + + var before = this.o.beforeShowDay(prevMonth); + if (before === undefined) + before = {}; + else if (typeof(before) === 'boolean') + before = {enabled: before}; + else if (typeof(before) === 'string') + before = {classes: before}; + if (before.enabled === false) + clsName.push('disabled'); + if (before.classes) + clsName = clsName.concat(before.classes.split(/\s+/)); + if (before.tooltip) + tooltip = before.tooltip; + + clsName = $.unique(clsName); + html.push('<td class="'+clsName.join(' ')+'"' + (tooltip ? ' title="'+tooltip+'"' : '') + '>'+prevMonth.getUTCDate() + '</td>'); + if (prevMonth.getUTCDay() == this.o.weekEnd) { html.push('</tr>'); } prevMonth.setUTCDate(prevMonth.getUTCDate()+1); @@ -514,7 +559,7 @@ .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>'; + html += '<span class="year'+(i == -1 ? ' old' : i == 10 ? ' new' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>'; year += 1; } yearCont.html(html); @@ -528,12 +573,12 @@ month = d.getUTCMonth(); switch (this.viewMode) { case 0: - if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) { + if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.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()) { + if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); @@ -541,12 +586,12 @@ break; case 1: case 2: - if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) { + if (this.o.startDate !== -Infinity && year <= this.o.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()) { + if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); @@ -562,7 +607,7 @@ switch(target[0].nodeName.toLowerCase()) { case 'th': switch(target[0].className) { - case 'switch': + case 'datepicker-switch': this.showMode(1); break; case 'prev': @@ -584,9 +629,22 @@ date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); this.showMode(-2); - var which = this.todayBtn == 'linked' ? null : 'view'; + var which = this.o.todayBtn == 'linked' ? null : 'view'; this._setDate(date, which); break; + case 'clear': + var element; + if (this.isInput) + element = this.element; + else if (this.component) + element = this.element.find('input'); + if (element) + element.val("").change(); + this._trigger('changeDate'); + this.update(); + if (this.o.autoclose) + this.hide(); + break; } break; case 'span': @@ -597,11 +655,8 @@ 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._trigger('changeMonth', this.viewDate); + if (this.o.minViewMode === 1) { this._setDate(UTCDate(year, month, day,0,0,0,0)); } } else { @@ -609,11 +664,8 @@ var day = 1; var month = 0; this.viewDate.setUTCFullYear(year); - this.element.trigger({ - type: 'changeYear', - date: this.viewDate - }); - if ( this.minViewMode == 2 ) { + this._trigger('changeYear', this.viewDate); + if (this.o.minViewMode === 2) { this._setDate(UTCDate(year, month, day,0,0,0,0)); } } @@ -650,15 +702,12 @@ _setDate: function(date, which){ if (!which || which == 'date') - this.date = date; + this.date = new Date(date); if (!which || which == 'view') - this.viewDate = date; + this.viewDate = new Date(date); this.fill(); this.setValue(); - this.element.trigger({ - type: 'changeDate', - date: this.date - }); + this._trigger('changeDate'); var element; if (this.isInput) { element = this.element; @@ -667,7 +716,7 @@ } if (element) { element.change(); - if (this.autoclose && (!which || which == 'date')) { + if (this.o.autoclose && (!which || which == 'date')) { this.hide(); } } @@ -718,7 +767,7 @@ }, dateWithinRange: function(date){ - return date >= this.startDate && date <= this.endDate; + return date >= this.o.startDate && date <= this.o.endDate; }, keydown: function(e){ @@ -737,7 +786,7 @@ break; case 37: // left case 39: // right - if (!this.keyboardNavigation) break; + if (!this.o.keyboardNavigation) break; dir = e.keyCode == 37 ? -1 : 1; if (e.ctrlKey){ newDate = this.moveYear(this.date, dir); @@ -762,7 +811,7 @@ break; case 38: // up case 40: // down - if (!this.keyboardNavigation) break; + if (!this.o.keyboardNavigation) break; dir = e.keyCode == 38 ? -1 : 1; if (e.ctrlKey){ newDate = this.moveYear(this.date, dir); @@ -794,10 +843,7 @@ break; } if (dateChanged){ - this.element.trigger({ - type: 'changeDate', - date: this.date - }); + this._trigger('changeDate'); var element; if (this.isInput) { element = this.element; @@ -812,7 +858,7 @@ showMode: function(dir) { if (dir) { - this.viewMode = Math.max(this.minViewMode, Math.min(2, this.viewMode + dir)); + this.viewMode = Math.max(this.o.minViewMode, Math.min(2, this.viewMode + dir)); } /* vitalets: fixing bug of very special conditions: @@ -829,24 +875,151 @@ } }; - $.fn.datepicker = function ( option ) { + var DateRangePicker = function(element, options){ + this.element = $(element); + this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; }); + delete options.inputs; + + $(this.inputs) + .datepicker(options) + .bind('changeDate', $.proxy(this.dateUpdated, this)); + + this.pickers = $.map(this.inputs, function(i){ return $(i).data('datepicker'); }); + this.updateDates(); + }; + DateRangePicker.prototype = { + updateDates: function(){ + this.dates = $.map(this.pickers, function(i){ return i.date; }); + this.updateRanges(); + }, + updateRanges: function(){ + var range = $.map(this.dates, function(d){ return d.valueOf(); }); + $.each(this.pickers, function(i, p){ + p.setRange(range); + }); + }, + dateUpdated: function(e){ + var dp = $(e.target).data('datepicker'), + new_date = dp.getUTCDate(), + i = $.inArray(e.target, this.inputs), + l = this.inputs.length; + if (i == -1) return; + + if (new_date < this.dates[i]){ + // Date being moved earlier/left + while (i>=0 && new_date < this.dates[i]){ + this.pickers[i--].setUTCDate(new_date); + } + } + else if (new_date > this.dates[i]){ + // Date being moved later/right + while (i<l && new_date > this.dates[i]){ + this.pickers[i++].setUTCDate(new_date); + } + } + this.updateDates(); + }, + remove: function(){ + $.map(this.pickers, function(p){ p.remove(); }); + delete this.element.data().datepicker; + } + }; + + function opts_from_el(el, prefix){ + // Derive options from element data-attrs + var data = $(el).data(), + out = {}, inkey, + replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'), + prefix = new RegExp('^' + prefix.toLowerCase()); + for (var key in data) + if (prefix.test(key)){ + inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); }); + out[inkey] = data[key]; + } + return out; + } + + function opts_from_locale(lang){ + // Derive options from locale plugins + var out = {}; + // Check if "de-DE" style date is available, if not language should + // fallback to 2 letter code eg "de" + if (!dates[lang]) { + lang = lang.split('-')[0] + if (!dates[lang]) + return; + } + var d = dates[lang]; + $.each(locale_opts, function(i,k){ + if (k in d) + out[k] = d[k]; + }); + return out; + } + + var old = $.fn.datepicker; + var datepicker = $.fn.datepicker = function ( option ) { var args = Array.apply(null, arguments); args.shift(); - return this.each(function () { + var internal_return, + this_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)))); + var elopts = opts_from_el(this, 'date'), + // Preliminary otions + xopts = $.extend({}, defaults, elopts, options), + locopts = opts_from_locale(xopts.language), + // Options priority: js args, data-attrs, locales, defaults + opts = $.extend({}, defaults, locopts, elopts, options); + if ($this.is('.input-daterange') || opts.inputs){ + var ropts = { + inputs: opts.inputs || $this.find('input').toArray() + }; + $this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts)))); + } + else{ + $this.data('datepicker', (data = new Datepicker(this, opts))); + } } if (typeof option == 'string' && typeof data[option] == 'function') { - data[option].apply(data, args); + internal_return = data[option].apply(data, args); + if (internal_return !== undefined) + return false; } }); + if (internal_return !== undefined) + return internal_return; + else + return this; }; - $.fn.datepicker.defaults = { + var defaults = $.fn.datepicker.defaults = { + autoclose: false, + beforeShowDay: $.noop, + calendarWeeks: false, + clearBtn: false, + daysOfWeekDisabled: [], + endDate: Infinity, + forceParse: true, + format: 'mm/dd/yyyy', + keyboardNavigation: true, + language: 'en', + minViewMode: 0, + rtl: false, + startDate: -Infinity, + startView: 0, + todayBtn: false, + todayHighlight: false, + weekStart: 0 }; + var locale_opts = $.fn.datepicker.locale_opts = [ + 'format', + 'rtl', + 'weekStart' + ]; $.fn.datepicker.Constructor = Datepicker; var dates = $.fn.datepicker.dates = { en: { @@ -855,7 +1028,8 @@ 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" + today: "Today", + clear: "Clear" } }; @@ -896,6 +1070,8 @@ }, parseDate: function(date, format, language) { if (date instanceof Date) return date; + if (typeof format === 'string') + format = DPGlobal.parseFormat(format); if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) { var part_re = /([\-+]\d+)([dmwy])/, parts = date.match(/([\-+]\d+)([dmwy])/g), @@ -986,6 +1162,8 @@ return date; }, formatDate: function(date, format, language){ + if (typeof format === 'string') + format = DPGlobal.parseFormat(format); var val = { d: date.getUTCDate(), D: dates[language].daysShort[date.getUTCDay()], @@ -1000,7 +1178,7 @@ 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++) { + for (var i=0, cnt = format.parts.length; i <= cnt; i++) { if (seps.length) date.push(seps.shift()); date.push(val[format.parts[i]]); @@ -1010,12 +1188,12 @@ headTemplate: '<thead>'+ '<tr>'+ '<th class="prev"><i class="icon-arrow-left"/></th>'+ - '<th colspan="5" class="switch"></th>'+ + '<th colspan="5" class="datepicker-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>' + footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr><tr><th colspan="7" class="clear"></th></tr></tfoot>' }; DPGlobal.template = '<div class="datepicker">'+ '<div class="datepicker-days">'+ @@ -1043,4 +1221,34 @@ $.fn.datepicker.DPGlobal = DPGlobal; -}( window.jQuery ); + + /* DATEPICKER NO CONFLICT + * =================== */ + + $.fn.datepicker.noConflict = function(){ + $.fn.datepicker = old; + return this; + }; + + + /* DATEPICKER DATA-API + * ================== */ + + $(document).on( + 'focus.datepicker.data-api click.datepicker.data-api', + '[data-provide="datepicker"]', + function(e){ + var $this = $(this); + if ($this.data('datepicker')) return; + e.preventDefault(); + // component click requires us to explicitly show it + datepicker.call($this, 'show'); + } + ); + $(function(){ + //$('[data-provide="datepicker-inline"]').datepicker(); + //vit: changed to support noConflict() + datepicker.call($('[data-provide="datepicker-inline"]')); + }); + +}( window.jQuery )); diff --git a/src/inputs/date/date.js b/src/inputs/date/date.js index a9d73f5..523f613 100644 --- a/src/inputs/date/date.js +++ b/src/inputs/date/date.js @@ -26,6 +26,12 @@ $(function(){ (function ($) { "use strict"; + //store bootstrap-datepicker as bdateicker to exclude conflict with jQuery UI one + $.fn.bdatepicker = $.fn.datepicker.noConflict(); + if(!$.fn.datepicker) { //if there were no other datepickers, keep also original name + $.fn.datepicker = $.fn.bdatepicker; + } + var Date = function (options) { this.init('date', options, Date.defaults); this.initPicker(options, Date.defaults); @@ -52,7 +58,7 @@ $(function(){ this.options.datepicker.language = this.options.datepicker.language || 'en'; //store DPglobal - this.dpg = $.fn.datepicker.DPGlobal; + this.dpg = $.fn.bdatepicker.DPGlobal; //store parsed formats this.parsedFormat = this.dpg.parseFormat(this.options.format); @@ -60,7 +66,7 @@ $(function(){ }, render: function () { - this.$input.datepicker(this.options.datepicker); + this.$input.bdatepicker(this.options.datepicker); //"clear" link if(this.options.clear) { @@ -75,55 +81,55 @@ $(function(){ }, value2html: function(value, element) { - var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''; + 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; + return this.parseDate(html, this.parsedViewFormat); }, - + 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); - }); + str2value: function(str) { + return this.parseDate(str, this.parsedFormat); + }, + + value2submit: function(value) { + return this.value2str(value); + }, + + value2input: function(value) { + this.$input.bdatepicker('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){ @@ -133,10 +139,29 @@ $(function(){ }, 200); }); */ + }, + + /* + For incorrect date bootstrap-datepicker returns current date that is not suitable + for datefield. + This function returns null for incorrect date. + */ + parseDate: function(str, format) { + var date = null, formattedBack; + if(str) { + date = this.dpg.parseDate(str, format, this.options.datepicker.language); + if(typeof str === 'string') { + formattedBack = this.dpg.formatDate(date, format, this.options.datepicker.language); + if(str !== formattedBack) { + date = null; + } + } + } + return date; } }); - + Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @@ -146,30 +171,30 @@ $(function(){ /** @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, + **/ + viewformat: null, /** Configuration of datepicker. Full list of options: http://vitalets.github.com/bootstrap-datepicker - + @property datepicker @type object @default { @@ -188,13 +213,13 @@ $(function(){ /** Text shown as clear date button. If <code>false</code> clear button will not be rendered. - + @property clear @type boolean|string - @default 'x clear' + @default 'x clear' **/ clear: '× clear' - }); + }); $.fn.editabletypes.date = Date; diff --git a/src/inputs/date/datefield.js b/src/inputs/date/datefield.js index bc5f070..9272b61 100644 --- a/src/inputs/date/datefield.js +++ b/src/inputs/date/datefield.js @@ -23,8 +23,9 @@ Automatically shown in inline mode. this.$input = this.$tpl.find('input'); this.setClass(); this.setAttr('placeholder'); - - this.$tpl.datepicker(this.options.datepicker); + + //bootstrap-datepicker is set `bdateicker` to exclude conflict with jQuery UI one. (in date.js) + this.$tpl.bdatepicker(this.options.datepicker); //need to disable original event handlers this.$input.off('focus keydown'); @@ -32,14 +33,14 @@ Automatically shown in inline mode. //update value of datepicker this.$input.keyup($.proxy(function(){ this.$tpl.removeData('date'); - this.$tpl.datepicker('update'); + this.$tpl.bdatepicker('update'); }, this)); }, value2input: function(value) { this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : ''); - this.$tpl.datepicker('update'); + this.$tpl.bdatepicker('update'); }, input2value: function() { diff --git a/src/inputs/datetime/bootstrap-datetimepicker/css/datetimepicker.css b/src/inputs/datetime/bootstrap-datetimepicker/css/datetimepicker.css index 73eeb25..18bd8be 100644 --- a/src/inputs/datetime/bootstrap-datetimepicker/css/datetimepicker.css +++ b/src/inputs/datetime/bootstrap-datetimepicker/css/datetimepicker.css @@ -32,7 +32,7 @@ top: 0; left: 0; } -.datetimepicker-dropdown:before { +[class*=" datetimepicker-dropdown"]:before { content: ''; display: inline-block; border-left: 7px solid transparent; @@ -40,40 +40,64 @@ border-bottom: 7px solid #ccc; border-bottom-color: rgba(0, 0, 0, 0.2); position: absolute; - top: -7px; - left: 6px; } -.datetimepicker-dropdown:after { +[class*=" datetimepicker-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; } -.datetimepicker-dropdown-left:before { +[class*=" datetimepicker-dropdown-top"]: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; + border-top: 7px solid #ccc; + border-top-color: rgba(0, 0, 0, 0.2); + border-bottom: 0; +} +[class*=" datetimepicker-dropdown-top"]:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid #ffffff; + border-bottom: 0; +} +.datetimepicker-dropdown-bottom-right:before { top: -7px; right: 6px; } -.datetimepicker-dropdown-left:after { - content: ''; - display: inline-block; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid #ffffff; - position: absolute; +.datetimepicker-dropdown-bottom-right:after { top: -6px; right: 7px; } +.datetimepicker-dropdown-bottom-left:before { + top: -7px; + left: 6px; +} +.datetimepicker-dropdown-bottom-left:after { + top: -6px; + left: 7px; +} +.datetimepicker-dropdown-top-right:before { + bottom: -7px; + right: 6px; +} +.datetimepicker-dropdown-top-right:after { + bottom: -6px; + right: 7px; +} +.datetimepicker-dropdown-top-left:before { + bottom: -7px; + left: 6px; +} +.datetimepicker-dropdown-top-left:after { + bottom: -6px; + left: 7px; +} .datetimepicker > div { display: none; } diff --git a/src/inputs/datetime/bootstrap-datetimepicker/js/bootstrap-datetimepicker.js b/src/inputs/datetime/bootstrap-datetimepicker/js/bootstrap-datetimepicker.js index 8e8b83d..bad0260 100644 --- a/src/inputs/datetime/bootstrap-datetimepicker/js/bootstrap-datetimepicker.js +++ b/src/inputs/datetime/bootstrap-datetimepicker/js/bootstrap-datetimepicker.js @@ -53,10 +53,17 @@ this.linkFormat = DPGlobal.parseFormat(options.linkFormat || this.element.data('link-format') || DPGlobal.getDefaultFormat(this.formatType, 'link'), this.formatType); this.minuteStep = options.minuteStep || this.element.data('minute-step') || 5; this.pickerPosition = options.pickerPosition || this.element.data('picker-position') || 'bottom-right'; - this.showMeridian = options.showMeridian || this.element.data('show-meridian') || false; - this.initialDate = options.initialDate || new Date(); + this.showMeridian = options.showMeridian || this.element.data('show-meridian') || false; + this.initialDate = options.initialDate || new Date(); this._attachEvents(); + + this.formatViewType = "datetime"; + if ('formatViewType' in options) { + this.formatViewType = options.formatViewType; + } else if ('formatViewType' in this.element.data()) { + this.formatViewType = this.element.data('formatViewType'); + } this.minView = 0; if ('minView' in options) { @@ -83,13 +90,13 @@ this.startViewMode = DPGlobal.convertViewMode(this.startViewMode); this.viewMode = this.startViewMode; - this.viewSelect = this.minView; - if ('viewSelect' in options) { - this.viewSelect = options.viewSelect; - } else if ('viewSelect' in this.element.data()) { - this.viewSelect = this.element.data('view-select'); - } - this.viewSelect = DPGlobal.convertViewMode(this.viewSelect); + this.viewSelect = this.minView; + if ('viewSelect' in options) { + this.viewSelect = options.viewSelect; + } else if ('viewSelect' in this.element.data()) { + this.viewSelect = this.element.data('view-select'); + } + this.viewSelect = DPGlobal.convertViewMode(this.viewSelect); this.forceParse = true; if ('forceParse' in options) { @@ -108,11 +115,7 @@ if (this.isInline) { this.picker.addClass('datetimepicker-inline'); } else { - if (this.component && this.pickerPosition == 'bottom-left') { - this.picker.addClass('datetimepicker-dropdown-left dropdown-menu'); - } else { - this.picker.addClass('datetimepicker-dropdown dropdown-menu'); - } + this.picker.addClass('datetimepicker-dropdown-' + this.pickerPosition + ' dropdown-menu'); } if (this.isRTL){ this.picker.addClass('datetimepicker-rtl'); @@ -301,15 +304,28 @@ } }, + setFormat: function(format) { + this.format = DPGlobal.parseFormat(format, this.formatType); + var element; + if (this.isInput) { + element = this.element; + } else if (this.component){ + element = this.element.find('input'); + } + if (element && element.val()) { + this.setValue(); + } + }, + setValue: function() { var formatted = this.getFormattedDate(); if (!this.isInput) { if (this.component){ - this.element.find('input').prop('value', formatted); + this.element.find('input').val(formatted); } this.element.data('date', formatted); } else { - this.element.prop('value', formatted); + this.element.val(formatted); } if (this.linkField) { $('#' + this.linkField).val(this.getFormattedDate(this.linkFormat)); @@ -354,21 +370,26 @@ 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, left; + return $(this).css('z-index') != 'auto'; + }).first().css('z-index'))+10; + var offset, top, left; if (this.component) { offset = this.component.offset(); left = offset.left; - if (this.pickerPosition == 'bottom-left') { + if (this.pickerPosition == 'bottom-left' || this.pickerPosition == 'top-left') { left += this.component.outerWidth() - this.picker.outerWidth(); } } else { offset = this.element.offset(); left = offset.left; } + if (this.pickerPosition == 'top-left' || this.pickerPosition == 'top-right') { + top = offset.top - this.picker.outerHeight(); + } else { + top = offset.top + this.height; + } this.picker.css({ - top: offset.top + this.height, + top: top, left: left, zIndex: zIndex }); @@ -380,7 +401,7 @@ date = arguments[0]; fromArgs = true; } else { - date = this.isInput ? this.element.prop('value') : this.element.data('date') || this.element.find('input').prop('value') || this.initialDate; + date = this.element.data('date') || (this.isInput ? this.element.val() : this.element.find('input').val()) || this.initialDate; } if (!date) { @@ -439,11 +460,22 @@ today = new Date(); this.picker.find('.datetimepicker-days thead th:eq(1)') .text(dates[this.language].months[month]+' '+year); - this.picker.find('.datetimepicker-hours thead th:eq(1)') - .text(dayMonth+' '+dates[this.language].months[month]+' '+year); - this.picker.find('.datetimepicker-minutes thead th:eq(1)') - .text(dayMonth+' '+dates[this.language].months[month]+' '+year); - this.picker.find('tfoot th.today') + if (this.formatViewType == "time") { + var hourConverted = hours % 12 ? hours % 12 : 12; + var hoursDisplay = (hourConverted < 10 ? '0' : '') + hourConverted; + var minutesDisplay = (minutes < 10 ? '0' : '') + minutes; + var meridianDisplay = dates[this.language].meridiem[hours < 12 ? 0 : 1]; + this.picker.find('.datetimepicker-hours thead th:eq(1)') + .text(hoursDisplay + ':' + minutesDisplay + ' ' + meridianDisplay.toUpperCase()); + this.picker.find('.datetimepicker-minutes thead th:eq(1)') + .text(hoursDisplay + ':' + minutesDisplay + ' ' + meridianDisplay.toUpperCase()); + } else { + this.picker.find('.datetimepicker-hours thead th:eq(1)') + .text(dayMonth + ' ' + dates[this.language].months[month] + ' ' + year); + this.picker.find('.datetimepicker-minutes thead th:eq(1)') + .text(dayMonth + ' ' + dates[this.language].months[month] + ' ' + year); + } + this.picker.find('tfoot th.today') .text(dates[this.language].today) .toggle(this.todayBtn !== false); this.updateNavArrows(); @@ -451,9 +483,9 @@ /*var prevMonth = UTCDate(year, month, 0,0,0,0,0); prevMonth.setUTCDate(prevMonth.getDate() - (prevMonth.getUTCDay() - this.weekStart + 7)%7);*/ 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); + 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(); @@ -492,7 +524,7 @@ this.picker.find('.datetimepicker-days tbody').empty().append(html.join('')); html = []; - var txt = '', meridian = '', meridianOld = ''; + var txt = '', meridian = '', meridianOld = ''; for (var i=0;i<24;i++) { var actual = UTCDate(year, month, dayMonth, i); clsName = ''; @@ -502,29 +534,29 @@ } else if (hours == i) { clsName += ' active'; } - if (this.showMeridian && dates[this.language].meridiem.length == 2) { - meridian = (i<12?dates[this.language].meridiem[0]:dates[this.language].meridiem[1]); - if (meridian != meridianOld) { - if (meridianOld != '') { - html.push('</fieldset>'); - } - html.push('<fieldset class="hour"><legend>'+meridian.toUpperCase()+'</legend>'); - } - meridianOld = meridian; - txt = (i%12?i%12:12); - html.push('<span class="hour'+clsName+' hour_'+(i<12?'am':'pm')+'">'+txt+'</span>'); - if (i == 23) { - html.push('</fieldset>'); - } - } else { - txt = i+':00'; - html.push('<span class="hour'+clsName+'">'+txt+'</span>'); - } + if (this.showMeridian && dates[this.language].meridiem.length == 2) { + meridian = (i<12?dates[this.language].meridiem[0]:dates[this.language].meridiem[1]); + if (meridian != meridianOld) { + if (meridianOld != '') { + html.push('</fieldset>'); + } + html.push('<fieldset class="hour"><legend>'+meridian.toUpperCase()+'</legend>'); + } + meridianOld = meridian; + txt = (i%12?i%12:12); + html.push('<span class="hour'+clsName+' hour_'+(i<12?'am':'pm')+'">'+txt+'</span>'); + if (i == 23) { + html.push('</fieldset>'); + } + } else { + txt = i+':00'; + html.push('<span class="hour'+clsName+'">'+txt+'</span>'); + } } this.picker.find('.datetimepicker-hours td').html(html.join('')); html = []; - txt = '', meridian = '', meridianOld = ''; + txt = '', meridian = '', meridianOld = ''; for(var i=0;i<60;i+=this.minuteStep) { var actual = UTCDate(year, month, dayMonth, hours, i, 0); clsName = ''; @@ -533,26 +565,26 @@ } else if (Math.floor(minutes/this.minuteStep) == Math.floor(i/this.minuteStep)) { clsName += ' active'; } - if (this.showMeridian && dates[this.language].meridiem.length == 2) { - meridian = (hours<12?dates[this.language].meridiem[0]:dates[this.language].meridiem[1]); - if (meridian != meridianOld) { - if (meridianOld != '') { - html.push('</fieldset>'); - } - html.push('<fieldset class="minute"><legend>'+meridian.toUpperCase()+'</legend>'); - } - meridianOld = meridian; - txt = (hours%12?hours%12:12); - //html.push('<span class="minute'+clsName+' minute_'+(hours<12?'am':'pm')+'">'+txt+'</span>'); - html.push('<span class="minute'+clsName+'">'+txt+':'+(i<10?'0'+i:i)+'</span>'); - if (i == 59) { - html.push('</fieldset>'); - } - } else { - txt = i+':00'; - //html.push('<span class="hour'+clsName+'">'+txt+'</span>'); - html.push('<span class="minute'+clsName+'">'+hours+':'+(i<10?'0'+i:i)+'</span>'); - } + if (this.showMeridian && dates[this.language].meridiem.length == 2) { + meridian = (hours<12?dates[this.language].meridiem[0]:dates[this.language].meridiem[1]); + if (meridian != meridianOld) { + if (meridianOld != '') { + html.push('</fieldset>'); + } + html.push('<fieldset class="minute"><legend>'+meridian.toUpperCase()+'</legend>'); + } + meridianOld = meridian; + txt = (hours%12?hours%12:12); + //html.push('<span class="minute'+clsName+' minute_'+(hours<12?'am':'pm')+'">'+txt+'</span>'); + html.push('<span class="minute'+clsName+'">'+txt+':'+(i<10?'0'+i:i)+'</span>'); + if (i == 59) { + html.push('</fieldset>'); + } + } else { + txt = i+':00'; + //html.push('<span class="hour'+clsName+'">'+txt+'</span>'); + html.push('<span class="minute'+clsName+'">'+hours+':'+(i<10?'0'+i:i)+'</span>'); + } } this.picker.find('.datetimepicker-minutes td').html(html.join('')); @@ -588,6 +620,7 @@ year += 1; } yearCont.html(html); + this.place(); }, updateNavArrows: function() { @@ -607,9 +640,9 @@ this.picker.find('.prev').css({visibility: 'visible'}); } if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() - && month >= this.endDate.getUTCMonth() - && day >= this.endDate.getUTCDate() - && hour >= this.endDate.getUTCHours()) { + && month >= this.endDate.getUTCMonth() + && day >= this.endDate.getUTCDate() + && hour >= this.endDate.getUTCHours()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); @@ -624,8 +657,8 @@ this.picker.find('.prev').css({visibility: 'visible'}); } if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() - && month >= this.endDate.getUTCMonth() - && day >= this.endDate.getUTCDate()) { + && month >= this.endDate.getUTCMonth() + && day >= this.endDate.getUTCDate()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); @@ -639,7 +672,7 @@ this.picker.find('.prev').css({visibility: 'visible'}); } if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() - && month >= this.endDate.getUTCMonth()) { + && month >= this.endDate.getUTCMonth()) { this.picker.find('.next').css({visibility: 'hidden'}); } else { this.picker.find('.next').css({visibility: 'visible'}); @@ -717,25 +750,25 @@ break; case 'span': if (!target.is('.disabled')) { - var year = this.viewDate.getUTCFullYear(), - month = this.viewDate.getUTCMonth(), - day = this.viewDate.getUTCDate(), - hours = this.viewDate.getUTCHours(), - minutes = this.viewDate.getUTCMinutes(), - seconds = this.viewDate.getUTCSeconds(); + var year = this.viewDate.getUTCFullYear(), + month = this.viewDate.getUTCMonth(), + day = this.viewDate.getUTCDate(), + hours = this.viewDate.getUTCHours(), + minutes = this.viewDate.getUTCMinutes(), + seconds = this.viewDate.getUTCSeconds(); if (target.is('.month')) { this.viewDate.setUTCDate(1); month = target.parent().find('span').index(target); - day = this.viewDate.getUTCDate(); + day = this.viewDate.getUTCDate(); this.viewDate.setUTCMonth(month); this.element.trigger({ type: 'changeMonth', date: this.viewDate }); - if (this.viewSelect >= 3) { - this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); - } + if (this.viewSelect >= 3) { + this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); + } } else if (target.is('.year')) { this.viewDate.setUTCDate(1); year = parseInt(target.text(), 10) || 0; @@ -744,36 +777,36 @@ type: 'changeYear', date: this.viewDate }); - if (this.viewSelect >= 4) { - this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); - } + if (this.viewSelect >= 4) { + this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); + } } else if (target.is('.hour')){ hours = parseInt(target.text(), 10) || 0; - if (target.hasClass('hour_am') || target.hasClass('hour_pm')) { - if (hours == 12 && target.hasClass('hour_am')) { - hours = 0; - } else if (hours != 12 && target.hasClass('hour_pm')) { - hours += 12; - } - } - this.viewDate.setUTCHours(hours); + if (target.hasClass('hour_am') || target.hasClass('hour_pm')) { + if (hours == 12 && target.hasClass('hour_am')) { + hours = 0; + } else if (hours != 12 && target.hasClass('hour_pm')) { + hours += 12; + } + } + this.viewDate.setUTCHours(hours); this.element.trigger({ type: 'changeHour', date: this.viewDate }); - if (this.viewSelect >= 1) { - this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); - } + if (this.viewSelect >= 1) { + this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); + } } else if (target.is('.minute')){ minutes = parseInt(target.text().substr(target.text().indexOf(':')+1), 10) || 0; - this.viewDate.setUTCMinutes(minutes); + this.viewDate.setUTCMinutes(minutes); this.element.trigger({ type: 'changeMinute', date: this.viewDate }); - if (this.viewSelect >= 0) { - this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); - } + if (this.viewSelect >= 0) { + this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); + } } if (this.viewMode != 0) { var oldViewMode = this.viewMode; @@ -813,16 +846,16 @@ month += 1; } } - this.viewDate.setUTCDate(day); - this.viewDate.setUTCMonth(month); - this.viewDate.setUTCFullYear(year); - this.element.trigger({ - type: 'changeDay', - date: this.viewDate - }); - if (this.viewSelect >= 2) { - this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); - } + this.viewDate.setUTCDate(day); + this.viewDate.setUTCMonth(month); + this.viewDate.setUTCFullYear(year); + this.element.trigger({ + type: 'changeDay', + date: this.viewDate + }); + if (this.viewSelect >= 2) { + this._setDate(UTCDate(year, month, day, hours, minutes, seconds, 0)); + } } var oldViewMode = this.viewMode; this.showMode(-1); @@ -841,10 +874,6 @@ if (!which || which == 'view') this.viewDate = date; this.fill(); - this.element.trigger({ - type: 'changeDate', - date: this.date - }); this.setValue(); var element; if (this.isInput) { @@ -858,6 +887,10 @@ //this.hide(); } } + this.element.trigger({ + type: 'changeDate', + date: this.date + }); }, moveMinute: function(date, dir){ @@ -950,28 +983,28 @@ case 39: // right if (!this.keyboardNavigation) break; dir = e.keyCode == 37 ? -1 : 1; - viewMode = this.viewMode; - if (e.ctrlKey) { - viewMode += 2; - } else if (e.shiftKey) { - viewMode += 1; - } - if (viewMode == 4) { + viewMode = this.viewMode; + if (e.ctrlKey) { + viewMode += 2; + } else if (e.shiftKey) { + viewMode += 1; + } + if (viewMode == 4) { newDate = this.moveYear(this.date, dir); newViewDate = this.moveYear(this.viewDate, dir); - } else if (viewMode == 3) { + } else if (viewMode == 3) { newDate = this.moveMonth(this.date, dir); newViewDate = this.moveMonth(this.viewDate, dir); - } else if (viewMode == 2) { + } else if (viewMode == 2) { newDate = this.moveDate(this.date, dir); newViewDate = this.moveDate(this.viewDate, dir); - } else if (viewMode == 1) { + } else if (viewMode == 1) { newDate = this.moveHour(this.date, dir); newViewDate = this.moveHour(this.viewDate, dir); - } else if (viewMode == 0) { + } else if (viewMode == 0) { newDate = this.moveMinute(this.date, dir); newViewDate = this.moveMinute(this.viewDate, dir); - } + } if (this.dateWithinRange(newDate)){ this.date = newDate; this.viewDate = newViewDate; @@ -985,33 +1018,33 @@ case 40: // down if (!this.keyboardNavigation) break; dir = e.keyCode == 38 ? -1 : 1; - viewMode = this.viewMode; - if (e.ctrlKey) { - viewMode += 2; - } else if (e.shiftKey) { - viewMode += 1; - } - if (viewMode == 4) { + viewMode = this.viewMode; + if (e.ctrlKey) { + viewMode += 2; + } else if (e.shiftKey) { + viewMode += 1; + } + if (viewMode == 4) { newDate = this.moveYear(this.date, dir); newViewDate = this.moveYear(this.viewDate, dir); - } else if (viewMode == 3) { + } else if (viewMode == 3) { newDate = this.moveMonth(this.date, dir); newViewDate = this.moveMonth(this.viewDate, dir); - } else if (viewMode == 2) { + } else if (viewMode == 2) { newDate = this.moveDate(this.date, dir * 7); newViewDate = this.moveDate(this.viewDate, dir * 7); - } else if (viewMode == 1) { - if (this.showMeridian) { - newDate = this.moveHour(this.date, dir * 6); - newViewDate = this.moveHour(this.viewDate, dir * 6); - } else { - newDate = this.moveHour(this.date, dir * 4); - newViewDate = this.moveHour(this.viewDate, dir * 4); - } - } else if (viewMode == 0) { + } else if (viewMode == 1) { + if (this.showMeridian) { + newDate = this.moveHour(this.date, dir * 6); + newViewDate = this.moveHour(this.viewDate, dir * 6); + } else { + newDate = this.moveHour(this.date, dir * 4); + newViewDate = this.moveHour(this.viewDate, dir * 4); + } + } else if (viewMode == 0) { newDate = this.moveMinute(this.date, dir * 4); newViewDate = this.moveMinute(this.viewDate, dir * 4); - } + } if (this.dateWithinRange(newDate)){ this.date = newDate; this.viewDate = newViewDate; @@ -1022,19 +1055,19 @@ } break; case 13: // enter - if (this.viewMode != 0) { - var oldViewMode = this.viewMode; - this.showMode(-1); - this.fill(); - if (oldViewMode == this.viewMode && this.autoclose) { - this.hide(); - } - } else { - this.fill(); - if (this.autoclose) { - this.hide(); - } - } + if (this.viewMode != 0) { + var oldViewMode = this.viewMode; + this.showMode(-1); + this.fill(); + if (oldViewMode == this.viewMode && this.autoclose) { + this.hide(); + } + } else { + this.fill(); + if (this.autoclose) { + this.hide(); + } + } e.preventDefault(); break; case 9: // tab @@ -1042,10 +1075,6 @@ break; } if (dateChanged){ - this.element.trigger({ - type: 'changeDate', - date: this.date - }); var element; if (this.isInput) { element = this.element; @@ -1055,6 +1084,10 @@ if (element) { element.change(); } + this.element.trigger({ + type: 'changeDate', + date: this.date + }); } }, @@ -1062,17 +1095,24 @@ if (dir) { var newViewMode = Math.max(0, Math.min(DPGlobal.modes.length - 1, this.viewMode + dir)); if (newViewMode >= this.minView && newViewMode <= this.maxView) { + this.element.trigger({ + type: 'changeMode', + date: this.viewDate, + oldViewMode: this.viewMode, + newViewMode: newViewMode + }); + this.viewMode = newViewMode; } } /* - vitalets: fixing bug of very special conditions: - jquery 1.7.1 + webkit + show inline datetimepicker in bootstrap popover. - Method show() does not set display css correctly and datetimepicker is not shown. - Changed to .css('display', 'block') solve the problem. - See https://github.com/vitalets/x-editable/issues/37 + vitalets: fixing bug of very special conditions: + jquery 1.7.1 + webkit + show inline datetimepicker in bootstrap popover. + Method show() does not set display css correctly and datetimepicker 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. + In jquery 1.7.2+ everything works fine. */ //this.picker.find('>div').hide().filter('.datetimepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); this.picker.find('>div').hide().filter('.datetimepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block'); @@ -1187,7 +1227,7 @@ parseDate: function(date, format, language, type) { if (date instanceof Date) { var dateUTC = new Date(date.valueOf() - date.getTimezoneOffset() * 60000); - dateUTC.setMilliseconds(0); + dateUTC.setMilliseconds(0); return dateUTC; } if (/^\d{4}\-\d{1,2}\-\d{1,2}$/.test(date)) { @@ -1254,7 +1294,7 @@ val, filtered, part; setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m']; setters_map['dd'] = setters_map['d']; - setters_map['P'] = setters_map['p']; + setters_map['P'] = setters_map['p']; date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()); if (parts.length == format.parts.length) { for (var i=0, cnt = format.parts.length; i < cnt; i++) { @@ -1278,10 +1318,10 @@ }); val = $.inArray(filtered[0], dates[language].monthsShort) + 1; break; - case 'p': - case 'P': - val = $.inArray(parts[i].toLowerCase(), dates[language].meridiem); - break; + case 'p': + case 'P': + val = $.inArray(parts[i].toLowerCase(), dates[language].meridiem); + break; } } parsed[part] = val; @@ -1320,9 +1360,9 @@ // second s: date.getUTCSeconds() }; - val.H = (val.h%12==0? 12 : val.h%12); - val.HH = (val.H < 10 ? '0' : '') + val.H; - val.P = val.p.toUpperCase(); + val.H = (val.h%12==0? 12 : val.h%12); + val.HH = (val.H < 10 ? '0' : '') + val.H; + val.P = val.p.toUpperCase(); val.hh = (val.h < 10 ? '0' : '') + val.h; val.ii = (val.i < 10 ? '0' : '') + val.i; val.ss = (val.s < 10 ? '0' : '') + val.s; diff --git a/src/inputs/datetime/datetime.js b/src/inputs/datetime/datetime.js index 6c68e34..786fe40 100644 --- a/src/inputs/datetime/datetime.js +++ b/src/inputs/datetime/datetime.js @@ -5,7 +5,7 @@ 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. @@ -30,14 +30,14 @@ $(function(){ **/ (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 @@ -46,13 +46,13 @@ $(function(){ 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'; @@ -62,16 +62,21 @@ $(function(){ //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); - + + //adjust container position when viewMode changes + //see https://github.com/smalot/bootstrap-datetimepicker/pull/80 + this.$input.on('changeMode', function(e) { + var f = $(this).closest('form').parent(); + //timeout here, otherwise container changes position before form has new size + setTimeout(function(){ + f.triggerHandler('resize'); + }, 0); + }); + //"clear" link if(this.options.clear) { this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){ @@ -79,11 +84,11 @@ $(function(){ 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) : ''; @@ -91,45 +96,45 @@ $(function(){ 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; + var value = this.parseDate(html, this.parsedViewFormat); 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; + var value = this.parseDate(str, this.parsedFormat); 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'); @@ -137,7 +142,7 @@ $(function(){ this.$input.closest('form').submit(); } }, - + autosubmit: function() { this.$input.on('mouseup', '.minute', function(e){ var $form = $(this).closest('form'); @@ -146,19 +151,38 @@ $(function(){ }, 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; + }, + + /* + For incorrect date bootstrap-datetimepicker returns current date that is not suitable + for datetimefield. + This function returns null for incorrect date. + */ + parseDate: function(str, format) { + var date = null, formattedBack; + if(str) { + date = this.dpg.parseDate(str, format, this.options.datetimepicker.language, this.options.formatType); + if(typeof str === 'string') { + formattedBack = this.dpg.formatDate(date, format, this.options.datetimepicker.language, this.options.formatType); + if(str !== formattedBack) { + date = null; + } + } + } + return date; } }); - + DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { /** @property tpl @@ -168,7 +192,7 @@ $(function(){ /** @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> @@ -187,12 +211,12 @@ $(function(){ @property viewformat @type string @default null - **/ - viewformat: null, + **/ + viewformat: null, /** Configuration of datetimepicker. Full list of options: https://github.com/smalot/bootstrap-datetimepicker - + @property datetimepicker @type object @default { } @@ -204,13 +228,13 @@ $(function(){ /** Text shown as clear date button. If <code>false</code> clear button will not be rendered. - + @property clear @type boolean|string - @default 'x clear' + @default 'x clear' **/ clear: '× clear' - }); + }); $.fn.editabletypes.datetime = DateTime; diff --git a/src/inputs/list.js b/src/inputs/list.js index 8f8afb0..1dc75fc 100644 --- a/src/inputs/list.js +++ b/src/inputs/list.js @@ -58,30 +58,33 @@ List - abstract class for inputs that have source option loaded from js array or // ------------- additional functions ------------ onSourceReady: function (success, error) { + //run source if it function + var source; + if ($.isFunction(this.options.source)) { + source = this.options.source.call(this.options.scope); + this.sourceData = null; + //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed + } else { + source = this.options.source; + } + //if allready loaded just call success - if($.isArray(this.sourceData)) { + if(this.options.sourceCache && $.isArray(this.sourceData)) { success.call(this); return; } - // try parse json in single quotes (for double quotes jquery does automatically) + //try parse json in single quotes (for double quotes jquery does automatically) try { - this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false); + source = $.fn.editableutils.tryParseJson(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 + //try to get sourceData from cache if(this.options.sourceCache) { var cacheID = source, cache; diff --git a/src/inputs/select2/lib/README.md b/src/inputs/select2/lib/README.md index 9e5b64e..12e2492 100644 --- a/src/inputs/select2/lib/README.md +++ b/src/inputs/select2/lib/README.md @@ -34,6 +34,7 @@ Integrations * [Django](https://github.com/applegrew/django-select2) * [Symfony](https://github.com/19Gerhard85/sfSelect2WidgetsPlugin) * [Bootstrap](https://github.com/t0m/select2-bootstrap-css) (CSS skin) +* [Yii](https://github.com/tonybolzan/yii-select2) Internationalization (i18n) --------------------------- diff --git a/src/inputs/select2/lib/component.json b/src/inputs/select2/lib/component.json index 2a46b66..4c7337f 100644 --- a/src/inputs/select2/lib/component.json +++ b/src/inputs/select2/lib/component.json @@ -1,8 +1,8 @@ { "name": "select2", - "version": "3.3.2", - "main": ["select2.js", "select2.css", "select2.png", "select2x2.png", "spinner.gif"], + "version": "3.4.0", + "main": ["select2.js", "select2.css", "select2.png", "select2x2.png", "select2-spinner.gif"], "dependencies": { - "jquery": "~1.4.4" + "jquery": ">= 1.7.1" } } diff --git a/src/inputs/select2/lib/select2.css b/src/inputs/select2/lib/select2.css index 36324d6..abe59c9 100644 --- a/src/inputs/select2/lib/select2.css +++ b/src/inputs/select2/lib/select2.css @@ -1,7 +1,8 @@ /* -Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 +Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 */ .select2-container { + margin: 0; position: relative; display: inline-block; /* inline-block for ie7 */ @@ -81,6 +82,10 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%); } +.select2-container.select2-allowclear .select2-choice span { + margin-right: 42px; +} + .select2-container .select2-choice span { margin-right: 26px; display: block; @@ -94,11 +99,11 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 } .select2-container .select2-choice abbr { - display: block; + display: none; width: 12px; height: 12px; position: absolute; - right: 26px; + right: 24px; top: 8px; font-size: 1px; @@ -109,6 +114,11 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 cursor: pointer; outline: 0; } + +.select2-container.select2-allowclear .select2-choice abbr { + display: inline-block; +} + .select2-container .select2-choice abbr:hover { background-position: right -11px; cursor: pointer; @@ -119,11 +129,6 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 left: 0; top: 0; z-index: 9998; - background-color: #fff; - opacity: 0; - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /* works in IE 8 */ - filter: "alpha(opacity=0)"; /* expected to work in IE 8 */ - filter: alpha(opacity=0); /* IE 4-7 */ } .select2-drop { @@ -147,6 +152,15 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 box-shadow: 0 4px 5px rgba(0, 0, 0, .15); } +.select2-drop-auto-width { + border-top: 1px solid #aaa; + width: auto; +} + +.select2-drop-auto-width .select2-search { + padding-top: 4px; +} + .select2-drop.select2-drop-above { margin-top: 1px; border-top: 1px solid #aaa; @@ -162,7 +176,7 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 } .select2-container .select2-choice div { - display: block; + display: inline-block; width: 18px; height: 100%; position: absolute; @@ -209,12 +223,6 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 white-space: nowrap; } -.select2-search-hidden { - display: block; - position: absolute; - left: -10000px; -} - .select2-search input { width: 100%; height: auto !important; @@ -292,6 +300,20 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%); } +.select2-dropdown-open.select2-drop-above .select2-choice, +.select2-dropdown-open.select2-drop-above .select2-choices { + border: 1px solid #5897fb; + border-top-color: transparent; + + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, white), color-stop(0.5, #eeeeee)); + background-image: -webkit-linear-gradient(center top, white 0%, #eeeeee 50%); + background-image: -moz-linear-gradient(center top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -ms-linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); + background-image: linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); +} + .select2-dropdown-open .select2-choice div { background: transparent; border-left: none; @@ -314,6 +336,7 @@ Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 .select2-results ul.select2-result-sub { margin: 0; + padding-left: 0; } .select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px } @@ -420,7 +443,7 @@ disabled look for disabled choices in the results dropdown } .select2-container.select2-container-disabled .select2-choice abbr { - display: none + display: none; } @@ -579,8 +602,8 @@ disabled look for disabled choices in the results dropdown background-color: #f4f4f4; } -.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { - display: none; +.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; + background:none; } /* end multiselect */ @@ -590,17 +613,31 @@ disabled look for disabled choices in the results dropdown text-decoration: underline; } -.select2-offscreen { - border: 0; +.select2-offscreen, .select2-offscreen:focus { clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; width: 1px; + height: 1px; + border: 0; + margin: 0; + padding: 0; + overflow: hidden; + position: absolute; + outline: 0; + left: 0px; } +.select2-display-none { + display: none; +} + +.select2-measure-scrollbar { + position: absolute; + top: -10000px; + left: -10000px; + width: 100px; + height: 100px; + overflow: scroll; +} /* Retina-ize icons */ @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { diff --git a/src/inputs/select2/lib/select2.jquery.json b/src/inputs/select2/lib/select2.jquery.json index 2a539e7..b9b114d 100644 --- a/src/inputs/select2/lib/select2.jquery.json +++ b/src/inputs/select2/lib/select2.jquery.json @@ -11,7 +11,7 @@ "tag", "tagging" ], - "version": "3.3.2", + "version": "3.4.0", "author": { "name": "Igor Vaynberg", "url": "https://github.com/ivaynberg" @@ -31,6 +31,6 @@ "docs": "http://ivaynberg.github.com/select2/", "download": "https://github.com/ivaynberg/select2/tags", "dependencies": { - "jquery": ">=1.4.6" + "jquery": ">=1.7.1" } } diff --git a/src/inputs/select2/lib/select2.js b/src/inputs/select2/lib/select2.js index 093d7fb..60f8086 100644 --- a/src/inputs/select2/lib/select2.js +++ b/src/inputs/select2/lib/select2.js @@ -1,7 +1,7 @@ -/* +/* Copyright 2012 Igor Vaynberg -Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 +Version: @@ver@@ Timestamp: @@timestamp@@ This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU General Public License version 2 (the "GPL License"). You may choose either license to govern your @@ -47,7 +47,7 @@ the specific language governing permissions and limitations under the Apache Lic } var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer, - lastMousePosition, $document; + lastMousePosition={x:0,y:0}, $document, scrollBarDimensions, KEY = { TAB: 9, @@ -95,7 +95,8 @@ the specific language governing permissions and limitations under the Apache Lic k = k.which ? k.which : k; return k >= 112 && k <= 123; } - }; + }, + MEASURE_SCROLLBAR_TEMPLATE = "<div class='select2-measure-scrollbar'></div>"; $document = $(document); @@ -109,6 +110,19 @@ the specific language governing permissions and limitations under the Apache Lic return -1; } + function measureScrollbar () { + var $template = $( MEASURE_SCROLLBAR_TEMPLATE ); + $template.appendTo('body'); + + var dim = { + width: $template.width() - $template[0].clientWidth, + height: $template.height() - $template[0].clientHeight + }; + $template.remove(); + + return dim; + } + /** * Compares equality of a and b * @param a @@ -143,12 +157,12 @@ the specific language governing permissions and limitations under the Apache Lic function installKeyUpChangeEvent(element) { var key="keyup-change-value"; - element.bind("keydown", function () { + element.on("keydown", function () { if ($.data(element, key) === undefined) { $.data(element, key, element.val()); } }); - element.bind("keyup", function () { + element.on("keyup", function () { var val= $.data(element, key); if (val !== undefined && element.val() !== val) { $.removeData(element, key); @@ -157,8 +171,9 @@ the specific language governing permissions and limitations under the Apache Lic }); } - $document.bind("mousemove", function (e) { - lastMousePosition = {x: e.pageX, y: e.pageY}; + $document.on("mousemove", function (e) { + lastMousePosition.x = e.pageX; + lastMousePosition.y = e.pageY; }); /** @@ -168,7 +183,7 @@ the specific language governing permissions and limitations under the Apache Lic * the elements under the pointer are scrolled. */ function installFilteredMouseMove(element) { - element.bind("mousemove", function (e) { + element.on("mousemove", function (e) { var lastpos = lastMousePosition; if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) { $(e.target).trigger("mousemove-filtered", e); @@ -213,7 +228,7 @@ the specific language governing permissions and limitations under the Apache Lic function installDebouncedScroll(threshold, element) { var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);}); - element.bind("scroll", function (e) { + element.on("scroll", function (e) { if (indexOf(e.target, element.get()) >= 0) notify(e); }); } @@ -248,6 +263,23 @@ the specific language governing permissions and limitations under the Apache Lic }, 0); } + function getCursorInfo(el) { + el = $(el)[0]; + var offset = 0; + var length = 0; + if ('selectionStart' in el) { + offset = el.selectionStart; + length = el.selectionEnd - offset; + } else if ('selection' in document) { + el.focus(); + var sel = document.selection.createRange(); + length = document.selection.createRange().text.length; + sel.moveStart('character', -el.value.length); + offset = sel.text.length - length; + } + return { offset: offset, length: length }; + } + function killEvent(event) { event.preventDefault(); event.stopPropagation(); @@ -324,15 +356,31 @@ the specific language governing permissions and limitations under the Apache Lic markup.push(escapeMarkup(text.substring(match + tl, text.length))); } + function defaultEscapeMarkup(markup) { + var replace_map = { + '\\': '\', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + "/": '/' + }; + + return String(markup).replace(/[&<>"'\/\\]/g, function (match) { + return replace_map[match]; + }); + } + /** * Produces an ajax-based query function * * @param options object containing configuration paramters + * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax * @param options.url url for the data * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url. * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified - * @param options.traditional a boolean flag that should be true if you wish to use the traditional style of param serialization for the ajax request * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2. * The expected format is an object containing the following keys: @@ -355,14 +403,20 @@ the specific language governing permissions and limitations under the Apache Lic var requestNumber = requestSequence, // this request's sequence number data = options.data, // ajax data function url = ajaxUrl, // ajax url string or function - transport = options.transport || $.ajax, - type = options.type || 'GET', // set type of request (GET or POST) - params = {}; + transport = options.transport || $.fn.select2.ajaxDefaults.transport, + // deprecated - to be removed in 4.0 - use params instead + deprecated = { + type: options.type || 'GET', // set type of request (GET or POST) + cache: options.cache || false, + jsonpCallback: options.jsonpCallback||undefined, + dataType: options.dataType||"json" + }, + params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated); data = data ? data.call(self, query.term, query.page, query.context) : null; url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url; - if( null !== handler) { handler.abort(); } + if (handler) { handler.abort(); } if (options.params) { if ($.isFunction(options.params)) { @@ -376,8 +430,6 @@ the specific language governing permissions and limitations under the Apache Lic url: url, dataType: options.dataType, data: data, - type: type, - cache: false, success: function (data) { if (requestNumber < requestSequence) { return; @@ -427,7 +479,7 @@ the specific language governing permissions and limitations under the Apache Lic text = dataItem.text; // if text is not a function we assume it to be a key name if (!$.isFunction(text)) { - dataText = data.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available + dataText = dataItem.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available text = function (item) { return item[dataText]; }; } } @@ -491,7 +543,7 @@ the specific language governing permissions and limitations under the Apache Lic function checkFormatter(formatter, formatterName) { if ($.isFunction(formatter)) return true; if (!formatter) return false; - throw new Error("formatterName must be a function or a falsy value"); + throw new Error(formatterName +" must be a function or a falsy value"); } function evaluate(val) { @@ -590,7 +642,7 @@ the specific language governing permissions and limitations under the Apache Lic // abstract init: function (opts) { - var results, search, resultsSelector = ".select2-results", mask; + var results, search, resultsSelector = ".select2-results", disabled, readonly; // prepare options this.opts = opts = this.prepareOpts(opts); @@ -603,7 +655,6 @@ the specific language governing permissions and limitations under the Apache Lic this.destroy(); } - this.enabled=true; this.container = this.createContainer(); this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid()); @@ -618,14 +669,12 @@ the specific language governing permissions and limitations under the Apache Lic this.container.css(evaluate(opts.containerCss)); this.container.addClass(evaluate(opts.containerCssClass)); - this.elementTabIndex = this.opts.element.attr("tabIndex"); + this.elementTabIndex = this.opts.element.attr("tabindex"); // swap container for the element this.opts.element .data("select2", this) - .addClass("select2-offscreen") - .bind("focus.select2", function() { $(this).select2("focus"); }) - .attr("tabIndex", "-1") + .attr("tabindex", "-1") .before(this.container); this.container.data("select2", this); @@ -636,8 +685,6 @@ the specific language governing permissions and limitations under the Apache Lic this.results = results = this.container.find(resultsSelector); this.search = search = this.container.find("input.select2-input"); - search.attr("tabIndex", this.elementTabIndex); - this.resultsPage = 0; this.context = null; @@ -645,10 +692,14 @@ the specific language governing permissions and limitations under the Apache Lic this.initContainer(); installFilteredMouseMove(this.results); - this.dropdown.delegate(resultsSelector, "mousemove-filtered touchstart touchmove touchend", this.bind(this.highlightUnderEvent)); + this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent)); installDebouncedScroll(80, this.results); - this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded)); + this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded)); + + // do not propagate change event from the search field out of the component + $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();}); + $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();}); // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel if ($.fn.mousewheel) { @@ -665,11 +716,11 @@ the specific language governing permissions and limitations under the Apache Lic } installKeyUpChangeEvent(search); - search.bind("keyup-change input paste", this.bind(this.updateResults)); - search.bind("focus", function () { search.addClass("select2-focused"); }); - search.bind("blur", function () { search.removeClass("select2-focused");}); + search.on("keyup-change input paste", this.bind(this.updateResults)); + search.on("focus", function () { search.addClass("select2-focused"); }); + search.on("blur", function () { search.removeClass("select2-focused");}); - this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) { + this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) { if ($(e.target).closest(".select2-result-selectable").length > 0) { this.highlightUnderEvent(e); this.selectHighlighted(e); @@ -679,7 +730,7 @@ the specific language governing permissions and limitations under the Apache Lic // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's // dom it will trigger the popup close, which is not what we want - this.dropdown.bind("click mouseup mousedown", function (e) { e.stopPropagation(); }); + this.dropdown.on("click mouseup mousedown", function (e) { e.stopPropagation(); }); if ($.isFunction(this.opts.initSelection)) { // initialize selection based on the current value of the source element @@ -690,31 +741,73 @@ the specific language governing permissions and limitations under the Apache Lic this.monitorSource(); } - if (opts.element.is(":disabled") || opts.element.is("[readonly='readonly']")) this.disable(); + if (opts.maximumInputLength !== null) { + this.search.attr("maxlength", opts.maximumInputLength); + } + + var disabled = opts.element.prop("disabled"); + if (disabled === undefined) disabled = false; + this.enable(!disabled); + + var readonly = opts.element.prop("readonly"); + if (readonly === undefined) readonly = false; + this.readonly(readonly); + + // Calculate size of scrollbar + scrollBarDimensions = scrollBarDimensions || measureScrollbar(); + + this.autofocus = opts.element.prop("autofocus") + opts.element.prop("autofocus", false); + if (this.autofocus) this.focus(); }, // abstract destroy: function () { - var select2 = this.opts.element.data("select2"); + var element=this.opts.element, select2 = element.data("select2"); if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; } if (select2 !== undefined) { - select2.container.remove(); select2.dropdown.remove(); - select2.opts.element + element .removeClass("select2-offscreen") .removeData("select2") - .unbind(".select2") - .attr({"tabIndex": this.elementTabIndex}) - .show(); + .off(".select2") + .prop("autofocus", this.autofocus || false); + if (this.elementTabIndex) { + element.attr({tabindex: this.elementTabIndex}); + } else { + element.removeAttr("tabindex"); + } + element.show(); + } + }, + + // abstract + optionToData: function(element) { + if (element.is("option")) { + return { + id:element.prop("value"), + text:element.text(), + element: element.get(), + css: element.attr("class"), + disabled: element.prop("disabled"), + locked: equal(element.attr("locked"), "locked") + }; + } else if (element.is("optgroup")) { + return { + text:element.attr("label"), + children:[], + element: element.get(), + css: element.attr("class") + }; } }, // abstract prepareOpts: function (opts) { - var element, select, idKey, ajaxUrl; + var element, select, idKey, ajaxUrl, self = this; element = opts.element; @@ -733,7 +826,7 @@ the specific language governing permissions and limitations under the Apache Lic opts = $.extend({}, { populateResults: function(container, results, query) { - var populate, data, result, children, id=this.opts.id, self=this; + var populate, data, result, children, id=this.opts.id; populate=function(results, container, depth) { @@ -801,16 +894,16 @@ the specific language governing permissions and limitations under the Apache Lic opts.query = this.bind(function (query) { var data = { results: [], more: false }, term = query.term, - children, firstChild, process; + children, placeholderOption, process; process=function(element, collection) { var group; if (element.is("option")) { if (query.matcher(term, element.text(), element)) { - collection.push({id:element.attr("value"), text:element.text(), element: element.get(), css: element.attr("class"), disabled: equal(element.attr("disabled"), "disabled") }); + collection.push(self.optionToData(element)); } } else if (element.is("optgroup")) { - group={text:element.attr("label"), children:[], element: element.get(), css: element.attr("class")}; + group=self.optionToData(element); element.children().each2(function(i, elm) { process(elm, group.children); }); if (group.children.length>0) { collection.push(group); @@ -822,9 +915,9 @@ the specific language governing permissions and limitations under the Apache Lic // ignore the placeholder option if there is one if (this.getPlaceholder() !== undefined && children.length > 0) { - firstChild = children[0]; - if ($(firstChild).text() === "") { - children=children.not(firstChild); + placeholderOption = this.getPlaceholderOption(); + if (placeholderOption) { + children=children.not(placeholderOption); } } @@ -881,7 +974,7 @@ the specific language governing permissions and limitations under the Apache Lic monitorSource: function () { var el = this.opts.element, sync; - el.bind("change.select2", this.bind(function (e) { + el.on("change.select2", this.bind(function (e) { if (this.opts.element.data("select2-change-triggered") !== true) { this.initSelection(); } @@ -893,19 +986,13 @@ the specific language governing permissions and limitations under the Apache Lic // sync enabled state - enabled = this.opts.element.attr("disabled") !== "disabled"; - readonly = this.opts.element.attr("readonly") === "readonly"; - - enabled = enabled && !readonly; - - if (this.enabled !== enabled) { - if (enabled) { - this.enable(); - } else { - this.disable(); - } - } + var disabled = el.prop("disabled"); + if (disabled === undefined) disabled = false; + this.enable(!disabled); + var readonly = el.prop("readonly"); + if (readonly === undefined) readonly = false; + this.readonly(readonly); syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass); this.container.addClass(evaluate(this.opts.containerCssClass)); @@ -916,17 +1003,31 @@ the specific language governing permissions and limitations under the Apache Lic }); // mozilla and IE - el.bind("propertychange.select2 DOMAttrModified.select2", sync); + el.on("propertychange.select2 DOMAttrModified.select2", sync); + + + // hold onto a reference of the callback to work around a chromium bug + if (this.mutationCallback === undefined) { + this.mutationCallback = function (mutations) { + mutations.forEach(sync); + } + } + // safari and chrome if (typeof WebKitMutationObserver !== "undefined") { if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; } - this.propertyObserver = new WebKitMutationObserver(function (mutations) { - mutations.forEach(sync); - }); + this.propertyObserver = new WebKitMutationObserver(this.mutationCallback); this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false }); } }, + // abstract + triggerSelect: function(data) { + var evt = $.Event("select2-selecting", { val: this.id(data), object: data }); + this.opts.element.trigger(evt); + return !evt.isDefaultPrevented(); + }, + /** * Triggers the change event on the source element */ @@ -950,24 +1051,46 @@ the specific language governing permissions and limitations under the Apache Lic this.opts.element.blur(); }, - // abstract - enable: function() { - if (this.enabled) return; - - this.enabled=true; - this.container.removeClass("select2-container-disabled"); - this.opts.element.removeAttr("disabled"); + //abstract + isInterfaceEnabled: function() + { + return this.enabledInterface === true; }, // abstract - disable: function() { - if (!this.enabled) return; + enableInterface: function() { + var enabled = this._enabled && !this._readonly, + disabled = !enabled; + if (enabled === this.enabledInterface) return false; + + this.container.toggleClass("select2-container-disabled", disabled); this.close(); + this.enabledInterface = enabled; - this.enabled=false; - this.container.addClass("select2-container-disabled"); - this.opts.element.attr("disabled", "disabled"); + return true; + }, + + // abstract + enable: function(enabled) { + if (enabled === undefined) enabled = true; + if (this._enabled === enabled) return false; + this._enabled = enabled; + + this.opts.element.prop("disabled", !enabled); + this.enableInterface(); + return true; + }, + + // abstract + readonly: function(enabled) { + if (enabled === undefined) enabled = false; + if (this._readonly === enabled) return false; + this._readonly = enabled; + + this.opts.element.prop("readonly", enabled); + this.enableInterface(); + return true; }, // abstract @@ -977,22 +1100,37 @@ the specific language governing permissions and limitations under the Apache Lic // abstract positionDropdown: function() { - var offset = this.container.offset(), + var $dropdown = this.dropdown, + offset = this.container.offset(), height = this.container.outerHeight(false), width = this.container.outerWidth(false), - dropHeight = this.dropdown.outerHeight(false), + dropHeight = $dropdown.outerHeight(false), viewPortRight = $(window).scrollLeft() + $(window).width(), viewportBottom = $(window).scrollTop() + $(window).height(), dropTop = offset.top + height, dropLeft = offset.left, enoughRoomBelow = dropTop + dropHeight <= viewportBottom, enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(), - dropWidth = this.dropdown.outerWidth(false), + dropWidth = $dropdown.outerWidth(false), enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight, - aboveNow = this.dropdown.hasClass("select2-drop-above"), + aboveNow = $dropdown.hasClass("select2-drop-above"), bodyOffset, above, - css; + css, + resultsListNode; + + if (this.opts.dropdownAutoWidth) { + resultsListNode = $('.select2-results', $dropdown)[0]; + $dropdown.addClass('select2-drop-auto-width'); + $dropdown.css('width', ''); + // Add scrollbar width to dropdown if vertical scrollbar is present + dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width); + dropWidth > width ? width = dropWidth : dropWidth = width; + enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight; + } + else { + this.container.removeClass('select2-drop-auto-width'); + } //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow); //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove); @@ -1022,11 +1160,11 @@ the specific language governing permissions and limitations under the Apache Lic if (above) { dropTop = offset.top - dropHeight; this.container.addClass("select2-drop-above"); - this.dropdown.addClass("select2-drop-above"); + $dropdown.addClass("select2-drop-above"); } else { this.container.removeClass("select2-drop-above"); - this.dropdown.removeClass("select2-drop-above"); + $dropdown.removeClass("select2-drop-above"); } css = $.extend({ @@ -1035,7 +1173,7 @@ the specific language governing permissions and limitations under the Apache Lic width: width }, evaluate(this.opts.dropdownCss)); - this.dropdown.css(css); + $dropdown.css(css); }, // abstract @@ -1044,7 +1182,9 @@ the specific language governing permissions and limitations under the Apache Lic if (this.opened()) return false; - event = $.Event("opening"); + if (this._enabled === false || this._readonly === true) return false; + + event = $.Event("select2-opening"); this.opts.element.trigger(event); return !event.isDefaultPrevented(); }, @@ -1067,7 +1207,7 @@ the specific language governing permissions and limitations under the Apache Lic if (!this.shouldOpen()) return false; - window.setTimeout(this.bind(this.opening), 1); + this.opening(); return true; }, @@ -1081,19 +1221,16 @@ the specific language governing permissions and limitations under the Apache Lic scroll = "scroll." + cid, resize = "resize."+cid, orient = "orientationchange."+cid, - mask; - - this.clearDropdownAlignmentPreference(); + mask, maskCss; this.container.addClass("select2-dropdown-open").addClass("select2-container-active"); + this.clearDropdownAlignmentPreference(); if(this.dropdown[0] !== this.body().children().last()[0]) { this.dropdown.detach().appendTo(this.body()); } - this.updateResults(true); - // create the dropdown mask if doesnt already exist mask = $("#select2-drop-mask"); if (mask.length == 0) { @@ -1101,7 +1238,7 @@ the specific language governing permissions and limitations under the Apache Lic mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask"); mask.hide(); mask.appendTo(this.body()); - mask.bind("mousedown touchstart", function (e) { + mask.on("mousedown touchstart click", function (e) { var dropdown = $("#select2-drop"), self; if (dropdown.length > 0) { self=dropdown.data("select2"); @@ -1109,6 +1246,8 @@ the specific language governing permissions and limitations under the Apache Lic self.selectHighlighted({noFocus: true}); } self.close(); + e.preventDefault(); + e.stopPropagation(); } }); } @@ -1123,26 +1262,26 @@ the specific language governing permissions and limitations under the Apache Lic this.dropdown.attr("id", "select2-drop"); // show the elements - mask.css(_makeMaskCss()); - mask.show(); + maskCss=_makeMaskCss(); + + mask.css(maskCss).show(); + this.dropdown.show(); this.positionDropdown(); this.dropdown.addClass("select2-drop-active"); - this.ensureHighlightVisible(); // attach listeners to events that can change the position of the container and thus require // the position of the dropdown to be updated as well so it does not come unglued from the container var that = this; this.container.parents().add(window).each(function () { - $(this).bind(resize+" "+scroll+" "+orient, function (e) { - $("#select2-drop-mask").css(_makeMaskCss()); + $(this).on(resize+" "+scroll+" "+orient, function (e) { + var maskCss=_makeMaskCss(); + $("#select2-drop-mask").css(maskCss); that.positionDropdown(); }); }); - this.focusSearch(); - function _makeMaskCss() { return { width : Math.max(document.documentElement.scrollWidth, $(window).width()), @@ -1161,7 +1300,7 @@ the specific language governing permissions and limitations under the Apache Lic orient = "orientationchange."+cid; // unbind event listeners - this.container.parents().add(window).each(function () { $(this).unbind(scroll).unbind(resize).unbind(orient); }); + this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); }); this.clearDropdownAlignmentPreference(); @@ -1170,9 +1309,21 @@ the specific language governing permissions and limitations under the Apache Lic this.dropdown.hide(); this.container.removeClass("select2-dropdown-open"); this.results.empty(); + + this.clearSearch(); this.search.removeClass("select2-active"); - this.opts.element.trigger($.Event("close")); + this.opts.element.trigger($.Event("select2-close")); + }, + + /** + * Opens control, sets input value, and updates results. + */ + // abstract + externalSearch: function (term) { + this.open(); + this.search.val(term); + this.updateResults(false); }, // abstract @@ -1203,7 +1354,7 @@ the specific language governing permissions and limitations under the Apache Lic return; } - children = this.findHighlightableChoices(); + children = this.findHighlightableChoices().find('.select2-result-label'); child = $(children[index]); @@ -1231,7 +1382,6 @@ the specific language governing permissions and limitations under the Apache Lic // abstract findHighlightableChoices: function() { - var h=this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)"); return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)"); }, @@ -1272,7 +1422,7 @@ the specific language governing permissions and limitations under the Apache Lic data = choice.data("select2-data"); if (data) { - this.opts.element.trigger({ type: "highlight", val: this.id(data), choice: data }); + this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data }); } }, @@ -1369,7 +1519,6 @@ the specific language governing permissions and limitations under the Apache Lic } function postRender() { - results.scrollTop(0); search.removeClass("select2-active"); self.positionDropdown(); } @@ -1394,6 +1543,7 @@ the specific language governing permissions and limitations under the Apache Lic } else { render(""); } + if (initial && this.showSearch) this.showSearch(true); return; } @@ -1467,7 +1617,7 @@ the specific language governing permissions and limitations under the Apache Lic postRender(); - this.opts.element.trigger({ type: "loaded", data:data }); + this.opts.element.trigger({ type: "select2-loaded", items: data }); })}); }, @@ -1504,15 +1654,34 @@ the specific language governing permissions and limitations under the Apache Lic if (data) { this.highlight(index); this.onSelect(data, options); + } else if (options.noFocus) { + this.close(); } }, // abstract getPlaceholder: function () { + var placeholderOption; return this.opts.element.attr("placeholder") || this.opts.element.attr("data-placeholder") || // jquery 1.4 compat this.opts.element.data("placeholder") || - this.opts.placeholder; + this.opts.placeholder || + ((placeholderOption = this.getPlaceholderOption()) !== undefined ? placeholderOption.text() : undefined); + }, + + // abstract + getPlaceholderOption: function() { + if (this.select) { + var firstOption = this.select.children().first(); + if (this.opts.placeholderOption !== undefined ) { + //Determine the placeholder option based on the specified placeholderOption setting + return (this.opts.placeholderOption === "first" && firstOption) || + (typeof this.opts.placeholderOption === "function" && this.opts.placeholderOption(this.select)); + } else if (firstOption.text() === "" && firstOption.val() === "") { + //No explicit placeholder option specified, use the first if it's blank + return firstOption; + } + } }, /** @@ -1537,7 +1706,7 @@ the specific language governing permissions and limitations under the Apache Lic attrs = style.split(';'); for (i = 0, l = attrs.length; i < l; i = i + 1) { matches = attrs[i].replace(/\s/g, '') - .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/); + .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i); if (matches !== null && matches.length >= 1) return matches[1]; } @@ -1577,13 +1746,13 @@ the specific language governing permissions and limitations under the Apache Lic "class": "select2-container" }).html([ "<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>", - " <span></span><abbr class='select2-search-choice-close' style='display:none;'></abbr>", + " <span> </span><abbr class='select2-search-choice-close'></abbr>", " <div><b></b></div>" , "</a>", "<input class='select2-focusser select2-offscreen' type='text'/>", - "<div class='select2-drop' style='display:none'>" , + "<div class='select2-drop select2-display-none'>" , " <div class='select2-search'>" , - " <input type='text' autocomplete='off' class='select2-input'/>" , + " <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'/>" , " </div>" , " <ul class='select2-results'>" , " </ul>" , @@ -1592,29 +1761,44 @@ the specific language governing permissions and limitations under the Apache Lic }, // single - disable: function() { - if (!this.enabled) return; - - this.parent.disable.apply(this, arguments); - - this.focusser.attr("disabled", "disabled"); - }, - - // single - enable: function() { - if (this.enabled) return; - - this.parent.enable.apply(this, arguments); - - this.focusser.removeAttr("disabled"); + enableInterface: function() { + if (this.parent.enableInterface.apply(this, arguments)) { + this.focusser.prop("disabled", !this.isInterfaceEnabled()); + } }, // single opening: function () { - this.parent.opening.apply(this, arguments); - this.focusser.attr("disabled", "disabled"); + var el, range, len; - this.opts.element.trigger($.Event("open")); + if (this.opts.minimumResultsForSearch >= 0) { + this.showSearch(true); + } + + this.parent.opening.apply(this, arguments); + + if (this.showSearchInput !== false) { + // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range + // all other browsers handle this just fine + + this.search.val(this.focusser.val()); + } + this.search.focus(); + // move the cursor to the end after focussing, otherwise it will be at the beginning and + // new text will appear *before* focusser.val() + el = this.search.get(0); + if (el.createTextRange) { + range = el.createTextRange(); + range.collapse(false); + range.select(); + } else if (el.setSelectionRange) { + len = this.search.val().length; + el.setSelectionRange(len, len); + } + + this.focusser.prop("disabled", true).val(""); + this.updateResults(true); + this.opts.element.trigger($.Event("select2-open")); }, // single @@ -1622,7 +1806,7 @@ the specific language governing permissions and limitations under the Apache Lic if (!this.opened()) return; this.parent.close.apply(this, arguments); this.focusser.removeAttr("disabled"); - focus(this.focusser); + this.focusser.focus(); }, // single @@ -1652,10 +1836,13 @@ the specific language governing permissions and limitations under the Apache Lic var selection, container = this.container, - dropdown = this.dropdown, - clickingInside = false; + dropdown = this.dropdown; - this.showSearch(this.opts.minimumResultsForSearch >= 0); + if (this.opts.minimumResultsForSearch < 0) { + this.showSearch(false); + } else { + this.showSearch(true); + } this.selection = selection = container.find(".select2-choice"); @@ -1663,11 +1850,14 @@ the specific language governing permissions and limitations under the Apache Lic // rewrite labels from original element to focusser this.focusser.attr("id", "s2id_autogen"+nextUid()); + $("label[for='" + this.opts.element.attr("id") + "']") .attr('for', this.focusser.attr('id')); - this.search.bind("keydown", this.bind(function (e) { - if (!this.enabled) return; + this.focusser.attr("tabindex", this.elementTabIndex); + + this.search.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) { // prevent the page from scrolling @@ -1681,11 +1871,13 @@ the specific language governing permissions and limitations under the Apache Lic this.moveHighlight((e.which === KEY.UP) ? -1 : 1); killEvent(e); return; - case KEY.TAB: case KEY.ENTER: this.selectHighlighted(); killEvent(e); return; + case KEY.TAB: + this.selectHighlighted({noFocus: true}); + return; case KEY.ESC: this.cancel(e); killEvent(e); @@ -1693,7 +1885,7 @@ the specific language governing permissions and limitations under the Apache Lic } })); - this.search.bind("blur", this.bind(function(e) { + this.search.on("blur", this.bind(function(e) { // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown. // without this the search field loses focus which is annoying if (document.activeElement === this.body().get(0)) { @@ -1703,8 +1895,8 @@ the specific language governing permissions and limitations under the Apache Lic } })); - this.focusser.bind("keydown", this.bind(function (e) { - if (!this.enabled) return; + this.focusser.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { return; @@ -1717,6 +1909,9 @@ the specific language governing permissions and limitations under the Apache Lic if (e.which == KEY.DOWN || e.which == KEY.UP || (e.which == KEY.ENTER && this.opts.openOnEnter)) { + + if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return; + this.open(); killEvent(e); return; @@ -1733,56 +1928,63 @@ the specific language governing permissions and limitations under the Apache Lic installKeyUpChangeEvent(this.focusser); - this.focusser.bind("keyup-change input", this.bind(function(e) { - if (this.opened()) return; - this.open(); - if (this.showSearchInput !== false) { - this.search.val(this.focusser.val()); + this.focusser.on("keyup-change input", this.bind(function(e) { + if (this.opts.minimumResultsForSearch >= 0) { + e.stopPropagation(); + if (this.opened()) return; + this.open(); } - this.focusser.val(""); - killEvent(e); })); - selection.delegate("abbr", "mousedown", this.bind(function (e) { - if (!this.enabled) return; + selection.on("mousedown", "abbr", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; this.clear(); killEventImmediately(e); this.close(); this.selection.focus(); })); - selection.bind("mousedown", this.bind(function (e) { - clickingInside = true; + selection.on("mousedown", this.bind(function (e) { + + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } if (this.opened()) { this.close(); - } else if (this.enabled) { + } else if (this.isInterfaceEnabled()) { this.open(); } killEvent(e); - - clickingInside = false; })); - dropdown.bind("mousedown", this.bind(function() { this.search.focus(); })); + dropdown.on("mousedown", this.bind(function() { this.search.focus(); })); - selection.bind("focus", this.bind(function(e) { + selection.on("focus", this.bind(function(e) { killEvent(e); })); - this.focusser.bind("focus", this.bind(function(){ + this.focusser.on("focus", this.bind(function(){ + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } this.container.addClass("select2-container-active"); - })).bind("blur", this.bind(function() { + })).on("blur", this.bind(function() { if (!this.opened()) { this.container.removeClass("select2-container-active"); + this.opts.element.trigger($.Event("select2-blur")); } })); - this.search.bind("focus", this.bind(function(){ + this.search.on("focus", this.bind(function(){ + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } this.container.addClass("select2-container-active"); - })) + })); this.initContainerWidth(); + this.opts.element.addClass("select2-offscreen"); this.setPlaceholder(); }, @@ -1791,13 +1993,14 @@ the specific language governing permissions and limitations under the Apache Lic clear: function(triggerChange) { var data=this.selection.data("select2-data"); if (data) { // guard against queued quick consecutive clicks - this.opts.element.val(""); + var placeholderOption = this.getPlaceholderOption(); + this.opts.element.val(placeholderOption ? placeholderOption.val() : ""); this.selection.find("span").empty(); this.selection.removeData("select2-data"); this.setPlaceholder(); if (triggerChange !== false){ - this.opts.element.trigger({ type: "removed", val: this.id(data), choice: data }); + this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data }); this.triggerChange({removed:data}); } } @@ -1809,7 +2012,8 @@ the specific language governing permissions and limitations under the Apache Lic // single initSelection: function () { var selected; - if (this.opts.element.val() === "" && this.opts.element.text() === "") { + if (this.isPlaceholderOptionSelected()) { + this.updateSelection([]); this.close(); this.setPlaceholder(); } else { @@ -1824,17 +2028,25 @@ the specific language governing permissions and limitations under the Apache Lic } }, + isPlaceholderOptionSelected: function() { + var placeholderOption; + return ((placeholderOption = this.getPlaceholderOption()) !== undefined && placeholderOption.is(':selected')) || + (this.opts.element.val() === "") || + (this.opts.element.val() === undefined) || + (this.opts.element.val() === null); + }, + // single prepareOpts: function () { - var opts = this.parent.prepareOpts.apply(this, arguments); + var opts = this.parent.prepareOpts.apply(this, arguments), + self=this; if (opts.element.get(0).tagName.toLowerCase() === "select") { // install the selection initializer opts.initSelection = function (element, callback) { var selected = element.find(":selected"); // a single select box always has a value, no need to null check 'selected' - if ($.isFunction(callback)) - callback({id: selected.attr("value"), text: selected.text(), element:selected}); + callback(self.optionToData(selected)); }; } else if ("data" in opts) { // install default initSelection when applied to hidden input and data is local @@ -1862,9 +2074,9 @@ the specific language governing permissions and limitations under the Apache Lic // single getPlaceholder: function() { - // if a placeholder is specified on a single select without the first empty option ignore it + // if a placeholder is specified on a single select without a valid placeholder option ignore it if (this.select) { - if (this.select.find("option").first().text() !== "") { + if (this.getPlaceholderOption() === undefined) { return undefined; } } @@ -1876,16 +2088,16 @@ the specific language governing permissions and limitations under the Apache Lic setPlaceholder: function () { var placeholder = this.getPlaceholder(); - if (this.opts.element.val() === "" && placeholder !== undefined) { + if (this.isPlaceholderOptionSelected() && placeholder !== undefined) { - // check for a first blank option if attached to a select - if (this.select && this.select.find("option:first").text() !== "") return; + // check for a placeholder option if attached to a select + if (this.select && this.getPlaceholderOption() === undefined) return; this.selection.find("span").html(this.opts.escapeMarkup(placeholder)); this.selection.addClass("select2-default"); - this.selection.find("abbr").hide(); + this.container.removeClass("select2-allowclear"); } }, @@ -1904,49 +2116,60 @@ the specific language governing permissions and limitations under the Apache Lic // and highlight it if (noHighlightUpdate !== false) { - this.highlight(selected); + if (initial === true && selected >= 0) { + this.highlight(selected); + } else { + this.highlight(0); + } } - // hide the search box if this is the first we got the results and there are a few of them + // hide the search box if this is the first we got the results and there are enough of them for search if (initial === true) { - var min=this.opts.minimumResultsForSearch; - showSearchInput = min < 0 ? false : countResults(data.results) >= min; - this.showSearch(showSearchInput); + var min = this.opts.minimumResultsForSearch; + if (min >= 0) { + this.showSearch(countResults(data.results) >= min); + } } - }, // single showSearch: function(showSearchInput) { + if (this.showSearchInput === showSearchInput) return; + this.showSearchInput = showSearchInput; - this.dropdown.find(".select2-search")[showSearchInput ? "removeClass" : "addClass"]("select2-search-hidden"); + this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput); + this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput); //add "select2-with-searchbox" to the container if search box is shown - $(this.dropdown, this.container)[showSearchInput ? "addClass" : "removeClass"]("select2-with-searchbox"); + $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput); }, // single onSelect: function (data, options) { - var old = this.opts.element.val(); + + if (!this.triggerSelect(data)) { return; } + + var old = this.opts.element.val(), + oldData = this.data(); this.opts.element.val(this.id(data)); this.updateSelection(data); - this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data }); + this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data }); this.close(); if (!options || !options.noFocus) this.selection.focus(); - if (!equal(old, this.id(data))) { this.triggerChange(); } + if (!equal(old, this.id(data))) { this.triggerChange({added:data,removed:oldData}); } }, // single updateSelection: function (data) { - var container=this.selection.find("span"), formatted; + var container=this.selection.find("span"), formatted, cssClass; this.selection.data("select2-data", data); @@ -1955,17 +2178,25 @@ the specific language governing permissions and limitations under the Apache Lic if (formatted !== undefined) { container.append(this.opts.escapeMarkup(formatted)); } + cssClass=this.opts.formatSelectionCssClass(data, container); + if (cssClass !== undefined) { + container.addClass(cssClass); + } this.selection.removeClass("select2-default"); if (this.opts.allowClear && this.getPlaceholder() !== undefined) { - this.selection.find("abbr").show(); + this.container.addClass("select2-allowclear"); } }, // single val: function () { - var val, triggerChange = false, data = null, self = this; + var val, + triggerChange = false, + data = null, + self = this, + oldData = this.data(); if (arguments.length === 0) { return this.opts.element.val(); @@ -1981,33 +2212,30 @@ the specific language governing permissions and limitations under the Apache Lic this.select .val(val) .find(":selected").each2(function (i, elm) { - data = {id: elm.attr("value"), text: elm.text(), element: elm.get(0)}; + data = self.optionToData(elm); return false; }); this.updateSelection(data); this.setPlaceholder(); if (triggerChange) { - this.triggerChange(); + this.triggerChange({added: data, removed:oldData}); } } else { - if (this.opts.initSelection === undefined) { - throw new Error("cannot call val() if initSelection() is not defined"); - } // val is an id. !val is true for [undefined,null,'',0] - 0 is legal if (!val && val !== 0) { this.clear(triggerChange); - if (triggerChange) { - this.triggerChange(); - } return; } + if (this.opts.initSelection === undefined) { + throw new Error("cannot call val() if initSelection() is not defined"); + } this.opts.element.val(val); this.opts.initSelection(this.opts.element, function(data){ self.opts.element.val(!data ? "" : self.id(data)); self.updateSelection(data); self.setPlaceholder(); if (triggerChange) { - self.triggerChange(); + self.triggerChange({added: data, removed:oldData}); } }); } @@ -2020,7 +2248,7 @@ the specific language governing permissions and limitations under the Apache Lic }, // single - data: function(value) { + data: function(value, triggerChange) { var data; if (arguments.length === 0) { @@ -2029,10 +2257,14 @@ the specific language governing permissions and limitations under the Apache Lic return data; } else { if (!value || value === "") { - this.clear(); + this.clear(triggerChange); } else { + data = this.data(); this.opts.element.val(!value ? "" : this.id(value)); this.updateSelection(value); + if (triggerChange) { + this.triggerChange({added: value, removed:data}); + } } } } @@ -2048,10 +2280,10 @@ the specific language governing permissions and limitations under the Apache Lic " <ul class='select2-choices'>", //"<li class='select2-search-choice'><span>California</span><a href="javascript:void(0)" class="select2-search-choice-close"></a></li>" , " <li class='select2-search-field'>" , - " <input type='text' autocomplete='off' class='select2-input'>" , + " <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'>" , " </li>" , "</ul>" , - "<div class='select2-drop select2-drop-multi' style='display:none;'>" , + "<div class='select2-drop select2-drop-multi select2-display-none'>" , " <ul class='select2-results'>" , " </ul>" , "</div>"].join("")); @@ -2060,7 +2292,8 @@ the specific language governing permissions and limitations under the Apache Lic // multi prepareOpts: function () { - var opts = this.parent.prepareOpts.apply(this, arguments); + var opts = this.parent.prepareOpts.apply(this, arguments), + self=this; // TODO validate placeholder is a string if specified @@ -2071,7 +2304,7 @@ the specific language governing permissions and limitations under the Apache Lic var data = []; element.find(":selected").each2(function (i, elm) { - data.push({id: elm.attr("value"), text: elm.text(), element: elm[0]}); + data.push(self.optionToData(elm)); }); callback(data); }; @@ -2092,7 +2325,21 @@ the specific language governing permissions and limitations under the Apache Lic return is_match; }, callback: !$.isFunction(callback) ? $.noop : function() { - callback(matches); + // reorder matches based on the order they appear in the ids array because right now + // they are in the order in which they appear in data array + var ordered = []; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + for (var j = 0; j < matches.length; j++) { + var match = matches[j]; + if (equal(id, opts.id(match))) { + ordered.push(match); + matches.splice(j, 1); + break; + } + } + } + callback(ordered); } }); }; @@ -2101,6 +2348,24 @@ the specific language governing permissions and limitations under the Apache Lic return opts; }, + selectChoice: function (choice) { + + var selected = this.container.find(".select2-search-choice-focus"); + if (selected.length && choice && choice[0] == selected[0]) { + + } else { + if (selected.length) { + this.opts.element.trigger("choice-deselected", selected); + } + selected.removeClass("select2-search-choice-focus"); + if (choice && choice.length) { + this.close(); + choice.addClass("select2-search-choice-focus"); + this.opts.element.trigger("choice-selected", choice); + } + } + }, + // multi initContainer: function () { @@ -2109,39 +2374,77 @@ the specific language governing permissions and limitations under the Apache Lic this.searchContainer = this.container.find(".select2-search-field"); this.selection = selection = this.container.find(selector); + var _this = this; + this.selection.on("mousedown", ".select2-search-choice", function (e) { + //killEvent(e); + _this.search[0].focus(); + _this.selectChoice($(this)); + }) + //.sortable({ + // items: " > li", + // tolerance: "pointer", + // revert: 100 + //}); + // rewrite labels from original element to focusser this.search.attr("id", "s2id_autogen"+nextUid()); $("label[for='" + this.opts.element.attr("id") + "']") .attr('for', this.search.attr('id')); - this.search.bind("input paste", this.bind(function() { - if (!this.enabled) return; + this.search.on("input paste", this.bind(function() { + if (!this.isInterfaceEnabled()) return; if (!this.opened()) { this.open(); } })); - this.search.bind("keydown", this.bind(function (e) { - if (!this.enabled) return; + this.search.attr("tabindex", this.elementTabIndex); - if (e.which === KEY.BACKSPACE && this.search.val() === "") { - this.close(); + this.keydowns = 0; + this.search.on("keydown", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; - var choices, - selected = selection.find(".select2-search-choice-focus"); - if (selected.length > 0) { + ++this.keydowns; + var selected = selection.find(".select2-search-choice-focus"); + var prev = selected.prev(".select2-search-choice:not(.select2-locked)"); + var next = selected.next(".select2-search-choice:not(.select2-locked)"); + var pos = getCursorInfo(this.search); + + if (selected.length && + (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) { + var selectedChoice = selected; + if (e.which == KEY.LEFT && prev.length) { + selectedChoice = prev; + } + else if (e.which == KEY.RIGHT) { + selectedChoice = next.length ? next : null; + } + else if (e.which === KEY.BACKSPACE) { this.unselect(selected.first()); this.search.width(10); - killEvent(e); - return; + selectedChoice = prev.length ? prev : next; + } else if (e.which == KEY.DELETE) { + this.unselect(selected.first()); + this.search.width(10); + selectedChoice = next.length ? next : null; + } else if (e.which == KEY.ENTER) { + selectedChoice = null; } - choices = selection.find(".select2-search-choice:not(.select2-locked)"); - if (choices.length > 0) { - choices.last().addClass("select2-search-choice-focus"); + this.selectChoice(selectedChoice); + killEvent(e); + if (!selectedChoice || !selectedChoice.length) { + this.open(); } + return; + } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1) + || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) { + + this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last()); + killEvent(e); + return; } else { - selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"); + this.selectChoice(null); } if (this.opened()) { @@ -2152,10 +2455,13 @@ the specific language governing permissions and limitations under the Apache Lic killEvent(e); return; case KEY.ENTER: - case KEY.TAB: this.selectHighlighted(); killEvent(e); return; + case KEY.TAB: + this.selectHighlighted({noFocus:true}); + this.close(); + return; case KEY.ESC: this.cancel(e); killEvent(e); @@ -2190,56 +2496,59 @@ the specific language governing permissions and limitations under the Apache Lic })); - this.search.bind("keyup", this.bind(this.resizeSearch)); + this.search.on("keyup", this.bind(function (e) { + this.keydowns = 0; + this.resizeSearch(); + }) + ); - this.search.bind("blur", this.bind(function(e) { + this.search.on("blur", this.bind(function(e) { this.container.removeClass("select2-container-active"); this.search.removeClass("select2-focused"); + this.selectChoice(null); if (!this.opened()) this.clearSearch(); e.stopImmediatePropagation(); + this.opts.element.trigger($.Event("select2-blur")); })); - this.container.delegate(selector, "mousedown", this.bind(function (e) { - if (!this.enabled) return; + this.container.on("click", selector, this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; if ($(e.target).closest(".select2-search-choice").length > 0) { // clicked inside a select2 search choice, do not open return; } + this.selectChoice(null); this.clearPlaceholder(); + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } this.open(); this.focusSearch(); e.preventDefault(); })); - this.container.delegate(selector, "focus", this.bind(function () { - if (!this.enabled) return; + this.container.on("focus", selector, this.bind(function () { + if (!this.isInterfaceEnabled()) return; + if (!this.container.hasClass("select2-container-active")) { + this.opts.element.trigger($.Event("select2-focus")); + } this.container.addClass("select2-container-active"); this.dropdown.addClass("select2-drop-active"); this.clearPlaceholder(); })); this.initContainerWidth(); + this.opts.element.addClass("select2-offscreen"); // set the placeholder if necessary this.clearSearch(); }, // multi - enable: function() { - if (this.enabled) return; - - this.parent.enable.apply(this, arguments); - - this.search.removeAttr("disabled"); - }, - - // multi - disable: function() { - if (!this.enabled) return; - - this.parent.disable.apply(this, arguments); - - this.search.attr("disabled", true); + enableInterface: function() { + if (this.parent.enableInterface.apply(this, arguments)) { + this.search.prop("disabled", !this.isInterfaceEnabled()); + } }, // multi @@ -2266,13 +2575,14 @@ the specific language governing permissions and limitations under the Apache Lic // multi clearSearch: function () { - var placeholder = this.getPlaceholder(); + var placeholder = this.getPlaceholder(), + maxWidth = this.getMaxSearchWidth(); if (placeholder !== undefined && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) { this.search.val(placeholder).addClass("select2-default"); // stretch the search box to full width of the container so as much of the placeholder is visible as possible // we could call this.resizeSearch(), but we do not because that requires a sizer and we do not want to create one so early because of a firefox bug, see #944 - this.search.width(this.getMaxSearchWidth()); + this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width")); } else { this.search.val("").width(10); } @@ -2294,7 +2604,9 @@ the specific language governing permissions and limitations under the Apache Lic this.focusSearch(); - this.opts.element.trigger($.Event("open")); + this.updateResults(true); + this.search.focus(); + this.opts.element.trigger($.Event("select2-open")); }, // multi @@ -2350,6 +2662,9 @@ the specific language governing permissions and limitations under the Apache Lic // multi onSelect: function (data, options) { + + if (!this.triggerSelect(data)) { return; } + this.addSelectedChoice(data); this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data }); @@ -2404,18 +2719,23 @@ the specific language governing permissions and limitations under the Apache Lic var choice = enableChoice ? enabledItem : disabledItem, id = this.id(data), val = this.getVal(), - formatted; + formatted, + cssClass; formatted=this.opts.formatSelection(data, choice.find("div")); if (formatted != undefined) { choice.find("div").replaceWith("<div>"+this.opts.escapeMarkup(formatted)+"</div>"); } + cssClass=this.opts.formatSelectionCssClass(data, choice.find("div")); + if (cssClass != undefined) { + choice.addClass(cssClass); + } if(enableChoice){ choice.find(".select2-search-choice-close") - .bind("mousedown", killEvent) - .bind("click dblclick", this.bind(function (e) { - if (!this.enabled) return; + .on("mousedown", killEvent) + .on("click dblclick", this.bind(function (e) { + if (!this.isInterfaceEnabled()) return; $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){ this.unselect($(e.target)); @@ -2424,8 +2744,8 @@ the specific language governing permissions and limitations under the Apache Lic this.focusSearch(); })).dequeue(); killEvent(e); - })).bind("focus", this.bind(function () { - if (!this.enabled) return; + })).on("focus", this.bind(function () { + if (!this.isInterfaceEnabled()) return; this.container.addClass("select2-container-active"); this.dropdown.addClass("select2-drop-active"); })); @@ -2472,7 +2792,7 @@ the specific language governing permissions and limitations under the Apache Lic }, // multi - postprocessResults: function () { + postprocessResults: function (data, initial, noHighlightUpdate) { var val = this.getVal(), choices = this.results.find(".select2-result"), compound = this.results.find(".select2-result-with-children"), @@ -2495,10 +2815,19 @@ the specific language governing permissions and limitations under the Apache Lic } }); - if (this.highlight() == -1){ + if (this.highlight() == -1 && noHighlightUpdate !== false){ self.highlight(0); } + //If all results are chosen render formatNoMAtches + if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){ + if(!data || data && !data.more && this.results.find(".select2-no-results").length === 0) { + if (checkFormatter(self.opts.formatNoMatches, "formatNoMatches")) { + this.results.append("<li class='select2-no-results'>" + self.opts.formatNoMatches(self.search.val()) + "</li>"); + } + } + } + }, // multi @@ -2563,18 +2892,36 @@ the specific language governing permissions and limitations under the Apache Lic }, // multi - val: function () { - var val, triggerChange = false, data = [], self=this; + buildChangeDetails: function (old, current) { + var current = current.slice(0), + old = old.slice(0); + + // remove intersection from each array + for (var i = 0; i < current.length; i++) { + for (var j = 0; j < old.length; j++) { + if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) { + current.splice(i, 1); + i--; + old.splice(j, 1); + j--; + } + } + } + + return {added: current, removed: old}; + }, + + + // multi + val: function (val, triggerChange) { + var oldData, self=this, changeDetails; if (arguments.length === 0) { return this.getVal(); } - val = arguments[0]; - - if (arguments.length > 1) { - triggerChange = arguments[1]; - } + oldData=this.data(); + if (!oldData.length) oldData=[]; // val is an id. !val is true for [undefined,null,'',0] - 0 is legal if (!val && val !== 0) { @@ -2582,7 +2929,7 @@ the specific language governing permissions and limitations under the Apache Lic this.updateSelection([]); this.clearSearch(); if (triggerChange) { - this.triggerChange(); + this.triggerChange({added: this.data(), removed: oldData}); } return; } @@ -2593,7 +2940,7 @@ the specific language governing permissions and limitations under the Apache Lic if (this.select) { this.opts.initSelection(this.select, this.bind(this.updateSelection)); if (triggerChange) { - this.triggerChange(); + this.triggerChange(this.buildChangeDetails(oldData, this.data())); } } else { if (this.opts.initSelection === undefined) { @@ -2601,12 +2948,12 @@ the specific language governing permissions and limitations under the Apache Lic } this.opts.initSelection(this.opts.element, function(data){ - var ids=$(data).map(self.id); + var ids=$.map(data, self.id); self.setVal(ids); self.updateSelection(data); self.clearSearch(); if (triggerChange) { - self.triggerChange(); + self.triggerChange(this.buildChangeDetails(oldData, this.data())); } }); } @@ -2647,19 +2994,23 @@ the specific language governing permissions and limitations under the Apache Lic }, // multi - data: function(values) { - var self=this, ids; + data: function(values, triggerChange) { + var self=this, ids, old; if (arguments.length === 0) { return this.selection .find(".select2-search-choice") .map(function() { return $(this).data("select2-data"); }) .get(); } else { + old = this.data(); if (!values) { values = []; } ids = $.map(values, function(e) { return self.opts.id(e); }); this.setVal(ids); this.updateSelection(values); this.clearSearch(); + if (triggerChange) { + this.triggerChange(this.buildChangeDetails(old, this.data())); + } } } }); @@ -2669,7 +3020,10 @@ the specific language governing permissions and limitations under the Apache Lic var args = Array.prototype.slice.call(arguments, 0), opts, select2, - value, multiple, allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "disable", "positionDropdown", "data"]; + method, value, multiple, + allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "dropdown", "onSortStart", "onSortEnd", "enable", "readonly", "positionDropdown", "data", "search"], + valueMethods = ["val", "opened", "isFocused", "container", "data"], + methodsMap = { search: "externalSearch" }; this.each(function () { if (args.length === 0 || typeof(args[0]) === "object") { @@ -2677,7 +3031,7 @@ the specific language governing permissions and limitations under the Apache Lic opts.element = $(this); if (opts.element.get(0).tagName.toLowerCase() === "select") { - multiple = opts.element.attr("multiple"); + multiple = opts.element.prop("multiple"); } else { multiple = opts.multiple || false; if ("tags" in opts) {opts.multiple = multiple = true;} @@ -2694,12 +3048,21 @@ the specific language governing permissions and limitations under the Apache Lic value = undefined; select2 = $(this).data("select2"); if (select2 === undefined) return; - if (args[0] === "container") { - value=select2.container; + + method=args[0]; + + if (method === "container") { + value = select2.container; + } else if (method === "dropdown") { + value = select2.dropdown; } else { - value = select2[args[0]].apply(select2, args.slice(1)); + if (methodsMap[method]) method = methodsMap[method]; + + value = select2[method].apply(select2, args.slice(1)); + } + if (indexOf(args[0], valueMethods) >= 0) { + return false; } - if (value !== undefined) {return false;} } else { throw "Invalid arguments to select2 plugin: " + args; } @@ -2729,6 +3092,7 @@ the specific language governing permissions and limitations under the Apache Lic return results; }, formatResultCssClass: function(data) {return undefined;}, + formatSelectionCssClass: function(data, container) {return undefined;}, formatNoMatches: function () { return "No matches found"; }, formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1? "" : "s"); }, formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); }, @@ -2746,27 +3110,22 @@ the specific language governing permissions and limitations under the Apache Lic separator: ",", tokenSeparators: [], tokenizer: defaultTokenizer, - escapeMarkup: function (markup) { - var replace_map = { - '\\': '\', - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - "/": '/' - }; - - return String(markup).replace(/[&<>"'\/\\]/g, function (match) { - return replace_map[match[0]]; - }); - }, + escapeMarkup: defaultEscapeMarkup, blurOnChange: false, selectOnBlur: false, adaptContainerCssClass: function(c) { return c; }, adaptDropdownCssClass: function(c) { return null; } }; + $.fn.select2.ajaxDefaults = { + transport: $.ajax, + params: { + type: "GET", + cache: false, + dataType: "json" + } + }; + // exports window.Select2 = { query: { @@ -2775,7 +3134,8 @@ the specific language governing permissions and limitations under the Apache Lic tags: tags }, util: { debounce: debounce, - markMatch: markMatch + markMatch: markMatch, + escapeMarkup: defaultEscapeMarkup }, "class": { "abstract": AbstractSelect2, "single": SingleSelect2, diff --git a/src/inputs/select2/lib/select2.min.js b/src/inputs/select2/lib/select2.min.js index dbabf71..d78d00f 100644 --- a/src/inputs/select2/lib/select2.min.js +++ b/src/inputs/select2/lib/select2.min.js @@ -1,7 +1,7 @@ /* Copyright 2012 Igor Vaynberg -Version: 3.3.2 Timestamp: Mon Mar 25 12:14:18 PDT 2013 +Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU General Public License version 2 (the "GPL License"). You may choose either license to govern your @@ -18,5 +18,5 @@ or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CO either express or implied. See the Apache License and the GPL License for the specific language governing permissions and limitations under the Apache License and the GPL License. */ -(function(a){a.fn.each2===void 0&&a.fn.extend({each2:function(b){for(var c=a([0]),d=-1,e=this.length;e>++d&&(c.context=c[0]=this[d])&&b.call(c[0],d,c)!==!1;);return this}})})(jQuery),function(a,b){"use strict";function k(a,b){for(var c=0,d=b.length;d>c;c+=1)if(l(a,b[c]))return c;return-1}function l(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function m(b,c){var d,e,f;if(null===b||1>b.length)return[];for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d}function n(a){return a.outerWidth(!1)-a.width()}function o(c){var d="keyup-change-value";c.bind("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.bind("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function p(c){c.bind("mousemove",function(c){var d=i;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function q(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function r(a){var c,b=!1;return function(){return b===!1&&(c=a(),b=!0),c}}function s(a,b){var c=q(a,function(a){b.trigger("scroll-debounced",a)});b.bind("scroll",function(a){k(a.target,b.get())>=0&&c(a)})}function t(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus(),a.is(":visible")&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function u(a){a.preventDefault(),a.stopPropagation()}function v(a){a.preventDefault(),a.stopImmediatePropagation()}function w(b){if(!h){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);h=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),h.attr("class","select2-sizer"),a("body").append(h)}return h.text(b.val()),h.width()}function x(b,c,d){var e,g,f=[];e=b.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=c.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(this))})),b.attr("class",f.join(" "))}function y(a,c,d,e){var f=a.toUpperCase().indexOf(c.toUpperCase()),g=c.length;return 0>f?(d.push(e(a)),b):(d.push(e(a.substring(0,f))),d.push("<span class='select2-match'>"),d.push(e(a.substring(f,f+g))),d.push("</span>"),d.push(e(a.substring(f+g,a.length))),b)}function z(b){var c,d=0,e=null,f=b.quietMillis||100,g=b.url,h=this;return function(i){window.clearTimeout(c),c=window.setTimeout(function(){d+=1;var c=d,f=b.data,j=g,k=b.transport||a.ajax,l=b.type||"GET",m={};f=f?f.call(h,i.term,i.page,i.context):null,j="function"==typeof j?j.call(h,i.term,i.page,i.context):j,null!==e&&e.abort(),b.params&&(a.isFunction(b.params)?a.extend(m,b.params.call(h)):a.extend(m,b.params)),a.extend(m,{url:j,dataType:b.dataType,data:f,type:l,cache:!1,success:function(a){if(!(d>c)){var e=b.results(a,i.page);i.callback(e)}}}),e=k.call(h,m)},f)}}function A(c){var e,f,d=c,g=function(a){return""+a.text};a.isArray(d)&&(f=d,d={results:f}),a.isFunction(d)===!1&&(f=d,d=function(){return f});var h=d();return h.text&&(g=h.text,a.isFunction(g)||(e=d.text,g=function(a){return a[e]})),function(c){var h,e=c.term,f={results:[]};return""===e?(c.callback(d()),b):(h=function(b,d){var f,i;if(b=b[0],b.children){f={};for(i in b)b.hasOwnProperty(i)&&(f[i]=b[i]);f.children=[],a(b.children).each2(function(a,b){h(b,f.children)}),(f.children.length||c.matcher(e,g(f),b))&&d.push(f)}else c.matcher(e,g(b),b)&&d.push(b)},a(d().results).each2(function(a,b){h(b,f.results)}),c.callback(f),b)}}function B(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]};a(d?c():c).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g)}}function C(b){if(a.isFunction(b))return!0;if(!b)return!1;throw Error("formatterName must be a function or a falsy value")}function D(b){return a.isFunction(b)?b():b}function E(b){var c=0;return a.each(b,function(a,b){b.children?c+=E(b.children):c++}),c}function F(a,c,d,e){var h,i,j,k,m,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||1>e.tokenSeparators.length)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(m=e.tokenSeparators[j],i=a.indexOf(m),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+m.length),h.length>0&&(h=e.createSearchChoice(h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(l(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:b}function G(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,h,i,j;c={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case c.LEFT:case c.RIGHT:case c.UP:case c.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case c.SHIFT:case c.CTRL:case c.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},j=a(document),g=function(){var a=1;return function(){return a++}}(),j.bind("mousemove",function(a){i={x:a.pageX,y:a.pageY}}),d=G(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,f=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&this.destroy(),this.enabled=!0,this.container=this.createContainer(),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+g()),this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.body=r(function(){return c.element.closest("body")}),x(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.css(D(c.containerCss)),this.container.addClass(D(c.containerCssClass)),this.elementTabIndex=this.opts.element.attr("tabIndex"),this.opts.element.data("select2",this).addClass("select2-offscreen").bind("focus.select2",function(){a(this).select2("focus")}).attr("tabIndex","-1").before(this.container),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),this.dropdown.addClass(D(c.dropdownCssClass)),this.dropdown.data("select2",this),this.results=d=this.container.find(f),this.search=e=this.container.find("input.select2-input"),e.attr("tabIndex",this.elementTabIndex),this.resultsPage=0,this.context=null,this.initContainer(),p(this.results),this.dropdown.delegate(f,"mousemove-filtered touchstart touchmove touchend",this.bind(this.highlightUnderEvent)),s(80,this.results),this.dropdown.delegate(f,"scroll-debounced",this.bind(this.loadMoreIfNeeded)),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),u(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),u(a))}),o(e),e.bind("keyup-change input paste",this.bind(this.updateResults)),e.bind("focus",function(){e.addClass("select2-focused")}),e.bind("blur",function(){e.removeClass("select2-focused")}),this.dropdown.delegate(f,"mouseup",this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.bind("click mouseup mousedown",function(a){a.stopPropagation()}),a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),(c.element.is(":disabled")||c.element.is("[readonly='readonly']"))&&this.disable()},destroy:function(){var a=this.opts.element.data("select2");this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),a!==b&&(a.container.remove(),a.dropdown.remove(),a.opts.element.removeClass("select2-offscreen").removeData("select2").unbind(".select2").attr({tabIndex:this.elementTabIndex}).show())},prepareOpts:function(c){var d,e,f,g;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw Error("Option '"+this+"' is not allowed for Select2 when attached to a <select> element.")}),c=a.extend({},{populateResults:function(d,e,f){var g,k=this.opts.id,l=this;g=function(d,e,h){var i,j,m,n,o,p,q,r,s,t;for(d=c.sortResults(d,e,f),i=0,j=d.length;j>i;i+=1)m=d[i],o=m.disabled===!0,n=!o&&k(m)!==b,p=m.children&&m.children.length>0,q=a("<li></li>"),q.addClass("select2-results-dept-"+h),q.addClass("select2-result"),q.addClass(n?"select2-result-selectable":"select2-result-unselectable"),o&&q.addClass("select2-disabled"),p&&q.addClass("select2-result-with-children"),q.addClass(l.opts.formatResultCssClass(m)),r=a(document.createElement("div")),r.addClass("select2-result-label"),t=c.formatResult(m,r,f,l.opts.escapeMarkup),t!==b&&r.html(t),q.append(r),p&&(s=a("<ul></ul>"),s.addClass("select2-result-sub"),g(m.children,s,h+1),q.append(s)),q.data("select2-data",m),e.append(q)},g(e,d,0)}},a.fn.select2.defaults,c),"function"!=typeof c.id&&(f=c.id,c.id=function(a){return a[f]}),a.isArray(c.element.data("select2Tags"))){if("tags"in c)throw"tags specified as both an attribute 'data-select2-tags' and in options of Select2 "+c.element.attr("id");c.tags=c.element.data("select2Tags")}if(e?(c.query=this.bind(function(c){var g,h,i,e={results:[],more:!1},f=c.term;i=function(a,b){var d;a.is("option")?c.matcher(f,a.text(),a)&&b.push({id:a.attr("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:l(a.attr("disabled"),"disabled")}):a.is("optgroup")&&(d={text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")},a.children().each2(function(a,b){i(b,d.children)}),d.children.length>0&&b.push(d))},g=d.children(),this.getPlaceholder()!==b&&g.length>0&&(h=g[0],""===a(h).text()&&(g=g.not(h))),g.each2(function(a,b){i(b,e.results)}),c.callback(e)}),c.id=function(a){return a.id},c.formatResultCssClass=function(a){return a.css}):"query"in c||("ajax"in c?(g=c.element.data("ajax-url"),g&&g.length>0&&(c.ajax.url=g),c.query=z.call(c.element,c.ajax)):"data"in c?c.query=A(c.data):"tags"in c&&(c.query=B(c.tags),c.createSearchChoice===b&&(c.createSearchChoice=function(a){return{id:a,text:a}}),c.initSelection===b&&(c.initSelection=function(d,e){var f=[];a(m(d.val(),c.separator)).each(function(){var d=this,e=this,g=c.tags;a.isFunction(g)&&(g=g()),a(g).each(function(){return l(this.id,d)?(e=this.text,!1):b}),f.push({id:d,text:e})}),e(f)}))),"function"!=typeof c.query)throw"query function not defined for Select2 "+c.element.attr("id");return c},monitorSource:function(){var b,a=this.opts.element;a.bind("change.select2",this.bind(function(){this.opts.element.data("select2-change-triggered")!==!0&&this.initSelection()})),b=this.bind(function(){var a,b;a="disabled"!==this.opts.element.attr("disabled"),b="readonly"===this.opts.element.attr("readonly"),a=a&&!b,this.enabled!==a&&(a?this.enable():this.disable()),x(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.addClass(D(this.opts.containerCssClass)),x(this.dropdown,this.opts.element,this.opts.adaptDropdownCssClass),this.dropdown.addClass(D(this.opts.dropdownCssClass))}),a.bind("propertychange.select2 DOMAttrModified.select2",b),"undefined"!=typeof WebKitMutationObserver&&(this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),this.propertyObserver=new WebKitMutationObserver(function(a){a.forEach(b)}),this.propertyObserver.observe(a.get(0),{attributes:!0,subtree:!1}))},triggerChange:function(b){b=b||{},b=a.extend({},b,{type:"change",val:this.val()}),this.opts.element.data("select2-change-triggered",!0),this.opts.element.trigger(b),this.opts.element.data("select2-change-triggered",!1),this.opts.element.click(),this.opts.blurOnChange&&this.opts.element.blur()},enable:function(){this.enabled||(this.enabled=!0,this.container.removeClass("select2-container-disabled"),this.opts.element.removeAttr("disabled"))},disable:function(){this.enabled&&(this.close(),this.enabled=!1,this.container.addClass("select2-container-disabled"),this.opts.element.attr("disabled","disabled"))},opened:function(){return this.container.hasClass("select2-dropdown-open")},positionDropdown:function(){var o,p,q,b=this.container.offset(),c=this.container.outerHeight(!1),d=this.container.outerWidth(!1),e=this.dropdown.outerHeight(!1),f=a(window).scrollLeft()+a(window).width(),g=a(window).scrollTop()+a(window).height(),h=b.top+c,i=b.left,j=g>=h+e,k=b.top-e>=this.body().scrollTop(),l=this.dropdown.outerWidth(!1),m=f>=i+l,n=this.dropdown.hasClass("select2-drop-above");"static"!==this.body().css("position")&&(o=this.body().offset(),h-=o.top,i-=o.left),n?(p=!0,!k&&j&&(p=!1)):(p=!1,!j&&k&&(p=!0)),m||(i=b.left+d-l),p?(h=b.top-e,this.container.addClass("select2-drop-above"),this.dropdown.addClass("select2-drop-above")):(this.container.removeClass("select2-drop-above"),this.dropdown.removeClass("select2-drop-above")),q=a.extend({top:h,left:i,width:d},D(this.opts.dropdownCss)),this.dropdown.css(q)},shouldOpen:function(){var b;return this.opened()?!1:(b=a.Event("opening"),this.opts.element.trigger(b),!b.isDefaultPrevented())},clearDropdownAlignmentPreference:function(){this.container.removeClass("select2-drop-above"),this.dropdown.removeClass("select2-drop-above")},open:function(){return this.shouldOpen()?(window.setTimeout(this.bind(this.opening),1),!0):!1},opening:function(){function h(){return{width:Math.max(document.documentElement.scrollWidth,a(window).width()),height:Math.max(document.documentElement.scrollHeight,a(window).height())}}var f,b=this.containerId,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.clearDropdownAlignmentPreference(),this.container.addClass("select2-dropdown-open").addClass("select2-container-active"),this.dropdown[0]!==this.body().children().last()[0]&&this.dropdown.detach().appendTo(this.body()),this.updateResults(!0),f=a("#select2-drop-mask"),0==f.length&&(f=a(document.createElement("div")),f.attr("id","select2-drop-mask").attr("class","select2-drop-mask"),f.hide(),f.appendTo(this.body()),f.bind("mousedown touchstart",function(){var d,c=a("#select2-drop");c.length>0&&(d=c.data("select2"),d.opts.selectOnBlur&&d.selectHighlighted({noFocus:!0}),d.close())})),this.dropdown.prev()[0]!==f[0]&&this.dropdown.before(f),a("#select2-drop").removeAttr("id"),this.dropdown.attr("id","select2-drop"),f.css(h()),f.show(),this.dropdown.show(),this.positionDropdown(),this.dropdown.addClass("select2-drop-active"),this.ensureHighlightVisible();var g=this;this.container.parents().add(window).each(function(){a(this).bind(d+" "+c+" "+e,function(){a("#select2-drop-mask").css(h()),g.positionDropdown()})}),this.focusSearch()},close:function(){if(this.opened()){var b=this.containerId,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.parents().add(window).each(function(){a(this).unbind(c).unbind(d).unbind(e)}),this.clearDropdownAlignmentPreference(),a("#select2-drop-mask").hide(),this.dropdown.removeAttr("id"),this.dropdown.hide(),this.container.removeClass("select2-dropdown-open"),this.results.empty(),this.clearSearch(),this.search.removeClass("select2-active"),this.opts.element.trigger(a.Event("close"))}},clearSearch:function(){},getMaximumSelectionSize:function(){return D(this.opts.maximumSelectionSize)},ensureHighlightVisible:function(){var d,e,f,g,h,i,j,c=this.results;if(e=this.highlight(),!(0>e)){if(0==e)return c.scrollTop(0),b;d=this.findHighlightableChoices(),f=a(d[e]),g=f.offset().top+f.outerHeight(!0),e===d.length-1&&(j=c.find("li.select2-more-results"),j.length>0&&(g=j.offset().top+j.outerHeight(!0))),h=c.offset().top+c.outerHeight(!0),g>h&&c.scrollTop(c.scrollTop()+(g-h)),i=f.offset().top-c.offset().top,0>i&&"none"!=f.css("display")&&c.scrollTop(c.scrollTop()+i)}},findHighlightableChoices:function(){return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)"),this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)")},moveHighlight:function(b){for(var c=this.findHighlightableChoices(),d=this.highlight();d>-1&&c.length>d;){d+=b;var e=a(c[d]);if(e.hasClass("select2-result-selectable")&&!e.hasClass("select2-disabled")&&!e.hasClass("select2-selected")){this.highlight(d);break}}},highlight:function(c){var e,f,d=this.findHighlightableChoices();return 0===arguments.length?k(d.filter(".select2-highlighted")[0],d.get()):(c>=d.length&&(c=d.length-1),0>c&&(c=0),this.results.find(".select2-highlighted").removeClass("select2-highlighted"),e=a(d[c]),e.addClass("select2-highlighted"),this.ensureHighlightVisible(),f=e.data("select2-data"),f&&this.opts.element.trigger({type:"highlight",val:this.id(f),choice:f}),b)},countSelectableResults:function(){return this.findHighlightableChoices().length},highlightUnderEvent:function(b){var c=a(b.target).closest(".select2-result-selectable");if(c.length>0&&!c.is(".select2-highlighted")){var d=this.findHighlightableChoices();this.highlight(d.index(c))}else 0==c.length&&this.results.find(".select2-highlighted").removeClass("select2-highlighted")},loadMoreIfNeeded:function(){var c,a=this.results,b=a.find("li.select2-more-results"),e=this.resultsPage+1,f=this,g=this.search.val(),h=this.context;0!==b.length&&(c=b.offset().top-a.offset().top-a.height(),this.opts.loadMorePadding>=c&&(b.addClass("select2-active"),this.opts.query({element:this.opts.element,term:g,page:e,context:h,matcher:this.opts.matcher,callback:this.bind(function(c){f.opened()&&(f.opts.populateResults.call(this,a,c.results,{term:g,page:e,context:h}),f.postprocessResults(c,!1,!1),c.more===!0?(b.detach().appendTo(a).text(f.opts.formatLoadMore(e+1)),window.setTimeout(function(){f.loadMoreIfNeeded()},10)):b.remove(),f.positionDropdown(),f.resultsPage=e,f.context=c.context)})})))},tokenize:function(){},updateResults:function(c){function m(){e.scrollTop(0),d.removeClass("select2-active"),h.positionDropdown()}function n(a){e.html(a),m()}var g,i,d=this.search,e=this.results,f=this.opts,h=this,j=d.val(),k=a.data(this.container,"select2-last-term");if((c===!0||!k||!l(j,k))&&(a.data(this.container,"select2-last-term",j),c===!0||this.showSearchInput!==!1&&this.opened())){var o=this.getMaximumSelectionSize();if(o>=1&&(g=this.data(),a.isArray(g)&&g.length>=o&&C(f.formatSelectionTooBig,"formatSelectionTooBig")))return n("<li class='select2-selection-limit'>"+f.formatSelectionTooBig(o)+"</li>"),b;if(d.val().length<f.minimumInputLength)return C(f.formatInputTooShort,"formatInputTooShort")?n("<li class='select2-no-results'>"+f.formatInputTooShort(d.val(),f.minimumInputLength)+"</li>"):n(""),b;if(f.maximumInputLength&&d.val().length>f.maximumInputLength)return C(f.formatInputTooLong,"formatInputTooLong")?n("<li class='select2-no-results'>"+f.formatInputTooLong(d.val(),f.maximumInputLength)+"</li>"):n(""),b;f.formatSearching&&0===this.findHighlightableChoices().length&&n("<li class='select2-searching'>"+f.formatSearching()+"</li>"),d.addClass("select2-active"),i=this.tokenize(),i!=b&&null!=i&&d.val(i),this.resultsPage=1,f.query({element:f.element,term:d.val(),page:this.resultsPage,context:null,matcher:f.matcher,callback:this.bind(function(g){var i;return this.opened()?(this.context=g.context===b?null:g.context,this.opts.createSearchChoice&&""!==d.val()&&(i=this.opts.createSearchChoice.call(null,d.val(),g.results),i!==b&&null!==i&&h.id(i)!==b&&null!==h.id(i)&&0===a(g.results).filter(function(){return l(h.id(this),h.id(i))}).length&&g.results.unshift(i)),0===g.results.length&&C(f.formatNoMatches,"formatNoMatches")?(n("<li class='select2-no-results'>"+f.formatNoMatches(d.val())+"</li>"),b):(e.empty(),h.opts.populateResults.call(this,e,g.results,{term:d.val(),page:this.resultsPage,context:null}),g.more===!0&&C(f.formatLoadMore,"formatLoadMore")&&(e.append("<li class='select2-more-results'>"+h.opts.escapeMarkup(f.formatLoadMore(this.resultsPage))+"</li>"),window.setTimeout(function(){h.loadMoreIfNeeded()},10)),this.postprocessResults(g,c),m(),this.opts.element.trigger({type:"loaded",data:g}),b)):(this.search.removeClass("select2-active"),b)})})}},cancel:function(){this.close()},blur:function(){this.opts.selectOnBlur&&this.selectHighlighted({noFocus:!0}),this.close(),this.container.removeClass("select2-container-active"),this.search[0]===document.activeElement&&this.search.blur(),this.clearSearch(),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus")},focusSearch:function(){t(this.search)},selectHighlighted:function(a){var b=this.highlight(),c=this.results.find(".select2-highlighted"),d=c.closest(".select2-result").data("select2-data");d&&(this.highlight(b),this.onSelect(d,a))},getPlaceholder:function(){return this.opts.element.attr("placeholder")||this.opts.element.attr("data-placeholder")||this.opts.element.data("placeholder")||this.opts.placeholder},initContainerWidth:function(){function c(){var c,d,e,f,g;if("off"===this.opts.width)return null;if("element"===this.opts.width)return 0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px";if("copy"===this.opts.width||"resolve"===this.opts.width){if(c=this.opts.element.attr("style"),c!==b)for(d=c.split(";"),f=0,g=d.length;g>f;f+=1)if(e=d[f].replace(/\s/g,"").match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/),null!==e&&e.length>=1)return e[1];return"resolve"===this.opts.width?(c=this.opts.element.css("width"),c.indexOf("%")>0?c:0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px"):null}return a.isFunction(this.opts.width)?this.opts.width():this.opts.width}var d=c.call(this);null!==d&&this.container.css("width",d)}}),e=G(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container"}).html(["<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>"," <span></span><abbr class='select2-search-choice-close' style='display:none;'></abbr>"," <div><b></b></div>","</a>","<input class='select2-focusser select2-offscreen' type='text'/>","<div class='select2-drop' style='display:none'>"," <div class='select2-search'>"," <input type='text' autocomplete='off' class='select2-input'/>"," </div>"," <ul class='select2-results'>"," </ul>","</div>"].join(""));return b},disable:function(){this.enabled&&(this.parent.disable.apply(this,arguments),this.focusser.attr("disabled","disabled"))},enable:function(){this.enabled||(this.parent.enable.apply(this,arguments),this.focusser.removeAttr("disabled"))},opening:function(){this.parent.opening.apply(this,arguments),this.focusser.attr("disabled","disabled"),this.opts.element.trigger(a.Event("open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.removeAttr("disabled"),t(this.focusser))},focus:function(){this.opened()?this.close():(this.focusser.removeAttr("disabled"),this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus()},initContainer:function(){var d,e=this.container,f=this.dropdown,h=!1;this.showSearch(this.opts.minimumResultsForSearch>=0),this.selection=d=e.find(".select2-choice"),this.focusser=e.find(".select2-focusser"),this.focusser.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.focusser.attr("id")),this.search.bind("keydown",this.bind(function(a){if(this.enabled){if(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)return u(a),b;switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),u(a),b;case c.TAB:case c.ENTER:return this.selectHighlighted(),u(a),b;case c.ESC:return this.cancel(a),u(a),b}}})),this.search.bind("blur",this.bind(function(){document.activeElement===this.body().get(0)&&window.setTimeout(this.bind(function(){this.search.focus()}),0)})),this.focusser.bind("keydown",this.bind(function(a){return!this.enabled||a.which===c.TAB||c.isControl(a)||c.isFunctionKey(a)||a.which===c.ESC?b:this.opts.openOnEnter===!1&&a.which===c.ENTER?(u(a),b):a.which==c.DOWN||a.which==c.UP||a.which==c.ENTER&&this.opts.openOnEnter?(this.open(),u(a),b):a.which==c.DELETE||a.which==c.BACKSPACE?(this.opts.allowClear&&this.clear(),u(a),b):b})),o(this.focusser),this.focusser.bind("keyup-change input",this.bind(function(a){this.opened()||(this.open(),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.focusser.val(""),u(a))})),d.delegate("abbr","mousedown",this.bind(function(a){this.enabled&&(this.clear(),v(a),this.close(),this.selection.focus())})),d.bind("mousedown",this.bind(function(a){h=!0,this.opened()?this.close():this.enabled&&this.open(),u(a),h=!1})),f.bind("mousedown",this.bind(function(){this.search.focus()})),d.bind("focus",this.bind(function(a){u(a)})),this.focusser.bind("focus",this.bind(function(){this.container.addClass("select2-container-active")})).bind("blur",this.bind(function(){this.opened()||this.container.removeClass("select2-container-active")})),this.search.bind("focus",this.bind(function(){this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.setPlaceholder()},clear:function(a){var b=this.selection.data("select2-data");b&&(this.opts.element.val(""),this.selection.find("span").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),a!==!1&&(this.opts.element.trigger({type:"removed",val:this.id(b),choice:b}),this.triggerChange({removed:b})))},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text())this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder())})}},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments);return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(b,c){var d=b.find(":selected");a.isFunction(c)&&c({id:d.attr("value"),text:d.text(),element:d})}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=l(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&""!==this.select.find("option").first().text()?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(""===this.opts.element.val()&&a!==b){if(this.select&&""!==this.select.find("option:first").text())return;this.selection.find("span").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.selection.find("abbr").hide()}},postprocessResults:function(a,c,d){var e=0,f=this,g=!0;if(this.findHighlightableChoices().each2(function(a,c){return l(f.id(c.data("select2-data")),f.opts.element.val())?(e=a,!1):b}),d!==!1&&this.highlight(e),c===!0){var h=this.opts.minimumResultsForSearch;g=0>h?!1:E(a.results)>=h,this.showSearch(g)}},showSearch:function(b){this.showSearchInput=b,this.dropdown.find(".select2-search")[b?"removeClass":"addClass"]("select2-search-hidden"),a(this.dropdown,this.container)[b?"addClass":"removeClass"]("select2-with-searchbox")},onSelect:function(a,b){var c=this.opts.element.val();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),this.close(),b&&b.noFocus||this.selection.focus(),l(c,this.id(a))||this.triggerChange()},updateSelection:function(a){var d,c=this.selection.find("span");this.selection.data("select2-data",a),c.empty(),d=this.opts.formatSelection(a,c),d!==b&&c.append(this.opts.escapeMarkup(d)),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.selection.find("abbr").show()},val:function(){var a,c=!1,d=null,e=this;if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find(":selected").each2(function(a,b){return d={id:b.attr("value"),text:b.text(),element:b.get(0)},!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange();else{if(this.opts.initSelection===b)throw Error("cannot call val() if initSelection() is not defined");if(!a&&0!==a)return this.clear(c),c&&this.triggerChange(),b;this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange()})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a){var c;return 0===arguments.length?(c=this.selection.data("select2-data"),c==b&&(c=null),c):(a&&""!==a?(this.opts.element.val(a?this.id(a):""),this.updateSelection(a)):this.clear(),b)}}),f=G(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi"}).html([" <ul class='select2-choices'>"," <li class='select2-search-field'>"," <input type='text' autocomplete='off' class='select2-input'>"," </li>","</ul>","<div class='select2-drop select2-drop-multi' style='display:none;'>"," <ul class='select2-results'>"," </ul>","</div>"].join(""));return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments);return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var c=[];a.find(":selected").each2(function(a,b){c.push({id:b.attr("value"),text:b.text(),element:b[0]})}),b(c)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=m(c.val(),b.separator),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return l(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},initContainer:function(){var e,d=".select2-choices";this.searchContainer=this.container.find(".select2-search-field"),this.selection=e=this.container.find(d),this.search.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.search.attr("id")),this.search.bind("input paste",this.bind(function(){this.enabled&&(this.opened()||this.open())})),this.search.bind("keydown",this.bind(function(a){if(this.enabled){if(a.which===c.BACKSPACE&&""===this.search.val()){this.close();var d,f=e.find(".select2-search-choice-focus");if(f.length>0)return this.unselect(f.first()),this.search.width(10),u(a),b;d=e.find(".select2-search-choice:not(.select2-locked)"),d.length>0&&d.last().addClass("select2-search-choice-focus")}else e.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");if(this.opened())switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),u(a),b;case c.ENTER:case c.TAB:return this.selectHighlighted(),u(a),b;case c.ESC:return this.cancel(a),u(a),b}if(a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.BACKSPACE&&a.which!==c.ESC){if(a.which===c.ENTER){if(this.opts.openOnEnter===!1)return;if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return}this.open(),(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)&&u(a),a.which===c.ENTER&&u(a)}}})),this.search.bind("keyup",this.bind(this.resizeSearch)),this.search.bind("blur",this.bind(function(a){this.container.removeClass("select2-container-active"),this.search.removeClass("select2-focused"),this.opened()||this.clearSearch(),a.stopImmediatePropagation()})),this.container.delegate(d,"mousedown",this.bind(function(b){this.enabled&&(a(b.target).closest(".select2-search-choice").length>0||(this.clearPlaceholder(),this.open(),this.focusSearch(),b.preventDefault())) -})),this.container.delegate(d,"focus",this.bind(function(){this.enabled&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.clearSearch()},enable:function(){this.enabled||(this.parent.enable.apply(this,arguments),this.search.removeAttr("disabled"))},disable:function(){this.enabled&&(this.parent.disable.apply(this,arguments),this.search.attr("disabled",!0))},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(this.getMaxSearchWidth())):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),this.opts.element.trigger(a.Event("open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){0>k(e.id(this),c)&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer(a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,b){this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()&&this.updateResults(!0),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),b&&b.noFocus||this.focusSearch()},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,d=!c.locked,e=a("<li class='select2-search-choice'> <div></div> <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a></li>"),f=a("<li class='select2-search-choice select2-locked'><div></div></li>"),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div")),j!=b&&g.find("div").replaceWith("<div>"+this.opts.escapeMarkup(j)+"</div>"),d&&g.find(".select2-search-choice-close").bind("mousedown",u).bind("click dblclick",this.bind(function(b){this.enabled&&(a(b.target).closest(".select2-search-choice").fadeOut("fast",this.bind(function(){this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),this.close(),this.focusSearch()})).dequeue(),u(b))})).bind("focus",this.bind(function(){this.enabled&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(a){var c,d,b=this.getVal();if(a=a.closest(".select2-search-choice"),0===a.length)throw"Invalid argument: "+a+". Must be .select2-search-choice";c=a.data("select2-data"),c&&(d=k(this.id(c),b),d>=0&&(b.splice(d,1),this.setVal(b),this.select&&this.postprocessResults()),a.remove(),this.opts.element.trigger({type:"removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))},postprocessResults:function(){var a=this.getVal(),b=this.results.find(".select2-result"),c=this.results.find(".select2-result-with-children"),d=this;b.each2(function(b,c){var e=d.id(c.data("select2-data"));k(e,a)>=0&&(c.addClass("select2-selected"),c.find(".select2-result-selectable").addClass("select2-selected"))}),c.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&d.highlight(0)},getMaxSearchWidth:function(){return this.selection.width()-n(this.search)},resizeSearch:function(){var a,b,c,d,e,f=n(this.search);a=w(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(e)},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),m(a,this.opts.separator))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){0>k(this,c)&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},val:function(){var c,d=!1,f=this;if(0===arguments.length)return this.getVal();if(c=arguments[0],arguments.length>1&&(d=arguments[1]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange(),b;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange();else{if(this.opts.initSelection===b)throw Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a(b).map(f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange()})}this.clearSearch()},onSortStart:function(){if(this.select)throw Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(c){var e,d=this;return 0===arguments.length?this.selection.find(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(c||(c=[]),e=a.map(c,function(a){return d.opts.id(a)}),this.setVal(e),this.updateSelection(c),this.clearSearch(),b)}}),a.fn.select2=function(){var d,g,h,i,c=Array.prototype.slice.call(arguments,0),j=["val","destroy","opened","open","close","focus","isFocused","container","onSortStart","onSortEnd","enable","disable","positionDropdown","data"];return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?i=d.element.attr("multiple"):(i=d.multiple||!1,"tags"in d&&(d.multiple=i=!0)),g=i?new f:new e,g.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(0>k(c[0],j))throw"Unknown method: "+c[0];if(h=b,g=a(this).data("select2"),g===b)return;if(h="container"===c[0]?g.container:g[c[0]].apply(g,c.slice(1)),h!==b)return!1}}),h===b?this:h},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return y(a.text,c.term,e,d),e.join("")},formatSelection:function(a){return a?a.text:b},sortResults:function(a){return a},formatResultCssClass:function(){return b},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results..."},formatSearching:function(){return"Searching..."},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a.id},matcher:function(a,b){return(""+b).toUpperCase().indexOf((""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:F,escapeMarkup:function(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return(a+"").replace(/[&<>"'\/\\]/g,function(a){return b[a[0]]})},blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null}},window.Select2={query:{ajax:z,local:A,tags:B},util:{debounce:q,markMatch:y},"class":{"abstract":d,single:e,multi:f}}}}(jQuery); \ No newline at end of file +(function(a){a.fn.each2===void 0&&a.fn.extend({each2:function(b){for(var c=a([0]),d=-1,e=this.length;e>++d&&(c.context=c[0]=this[d])&&b.call(c[0],d,c)!==!1;);return this}})})(jQuery),function(a,b){"use strict";function m(a,b){for(var c=0,d=b.length;d>c;c+=1)if(o(a,b[c]))return c;return-1}function n(){var b=a(l);b.appendTo("body");var c={width:b.width()-b[0].clientWidth,height:b.height()-b[0].clientHeight};return b.remove(),c}function o(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function p(b,c){var d,e,f;if(null===b||1>b.length)return[];for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d}function q(a){return a.outerWidth(!1)-a.width()}function r(c){var d="keyup-change-value";c.on("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.on("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function s(c){c.on("mousemove",function(c){var d=i;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function t(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function u(a){var c,b=!1;return function(){return b===!1&&(c=a(),b=!0),c}}function v(a,b){var c=t(a,function(a){b.trigger("scroll-debounced",a)});b.on("scroll",function(a){m(a.target,b.get())>=0&&c(a)})}function w(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus(),a.is(":visible")&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function x(b){b=a(b)[0];var c=0,d=0;if("selectionStart"in b)c=b.selectionStart,d=b.selectionEnd-c;else if("selection"in document){b.focus();var e=document.selection.createRange();d=document.selection.createRange().text.length,e.moveStart("character",-b.value.length),c=e.text.length-d}return{offset:c,length:d}}function y(a){a.preventDefault(),a.stopPropagation()}function z(a){a.preventDefault(),a.stopImmediatePropagation()}function A(b){if(!h){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);h=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),h.attr("class","select2-sizer"),a("body").append(h)}return h.text(b.val()),h.width()}function B(b,c,d){var e,g,f=[];e=b.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=c.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(this))})),b.attr("class",f.join(" "))}function C(a,c,d,e){var f=a.toUpperCase().indexOf(c.toUpperCase()),g=c.length;return 0>f?(d.push(e(a)),b):(d.push(e(a.substring(0,f))),d.push("<span class='select2-match'>"),d.push(e(a.substring(f,f+g))),d.push("</span>"),d.push(e(a.substring(f+g,a.length))),b)}function D(c){var d,e=0,f=null,g=c.quietMillis||100,h=c.url,i=this;return function(j){window.clearTimeout(d),d=window.setTimeout(function(){e+=1;var d=e,g=c.data,k=h,l=c.transport||a.fn.select2.ajaxDefaults.transport,m={type:c.type||"GET",cache:c.cache||!1,jsonpCallback:c.jsonpCallback||b,dataType:c.dataType||"json"},n=a.extend({},a.fn.select2.ajaxDefaults.params,m);g=g?g.call(i,j.term,j.page,j.context):null,k="function"==typeof k?k.call(i,j.term,j.page,j.context):k,null!==f&&f.abort(),c.params&&(a.isFunction(c.params)?a.extend(n,c.params.call(i)):a.extend(n,c.params)),a.extend(n,{url:k,dataType:c.dataType,data:g,success:function(a){if(!(e>d)){var b=c.results(a,j.page);j.callback(b)}}}),f=l.call(i,n)},g)}}function E(c){var e,f,d=c,g=function(a){return""+a.text};a.isArray(d)&&(f=d,d={results:f}),a.isFunction(d)===!1&&(f=d,d=function(){return f});var h=d();return h.text&&(g=h.text,a.isFunction(g)||(e=h.text,g=function(a){return a[e]})),function(c){var h,e=c.term,f={results:[]};return""===e?(c.callback(d()),b):(h=function(b,d){var f,i;if(b=b[0],b.children){f={};for(i in b)b.hasOwnProperty(i)&&(f[i]=b[i]);f.children=[],a(b.children).each2(function(a,b){h(b,f.children)}),(f.children.length||c.matcher(e,g(f),b))&&d.push(f)}else c.matcher(e,g(b),b)&&d.push(b)},a(d().results).each2(function(a,b){h(b,f.results)}),c.callback(f),b)}}function F(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]};a(d?c():c).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g)}}function G(b){if(a.isFunction(b))return!0;if(!b)return!1;throw Error("formatterName must be a function or a falsy value")}function H(b){return a.isFunction(b)?b():b}function I(b){var c=0;return a.each(b,function(a,b){b.children?c+=I(b.children):c++}),c}function J(a,c,d,e){var h,i,j,k,l,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||1>e.tokenSeparators.length)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(l=e.tokenSeparators[j],i=a.indexOf(l),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+l.length),h.length>0&&(h=e.createSearchChoice(h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(o(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:b}function K(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,h,i,j,k,c={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case c.LEFT:case c.RIGHT:case c.UP:case c.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case c.SHIFT:case c.CTRL:case c.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},l="<div class='select2-measure-scrollbar'></div>";j=a(document),g=function(){var a=1;return function(){return a++}}(),j.on("mousemove",function(a){i={x:a.pageX,y:a.pageY}}),d=K(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,h,i,f=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&this.destroy(),this.container=this.createContainer(),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+g()),this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.body=u(function(){return c.element.closest("body")}),B(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.css(H(c.containerCss)),this.container.addClass(H(c.containerCssClass)),this.elementTabIndex=this.opts.element.attr("tabindex"),this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),this.dropdown.addClass(H(c.dropdownCssClass)),this.dropdown.data("select2",this),this.results=d=this.container.find(f),this.search=e=this.container.find("input.select2-input"),this.resultsPage=0,this.context=null,this.initContainer(),s(this.results),this.dropdown.on("mousemove-filtered touchstart touchmove touchend",f,this.bind(this.highlightUnderEvent)),v(80,this.results),this.dropdown.on("scroll-debounced",f,this.bind(this.loadMoreIfNeeded)),a(this.container).on("change",".select2-input",function(a){a.stopPropagation()}),a(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()}),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),y(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),y(a))}),r(e),e.on("keyup-change input paste",this.bind(this.updateResults)),e.on("focus",function(){e.addClass("select2-focused")}),e.on("blur",function(){e.removeClass("select2-focused")}),this.dropdown.on("mouseup",f,this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.on("click mouseup mousedown",function(a){a.stopPropagation()}),a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),null!==c.maximumInputLength&&this.search.attr("maxlength",c.maximumInputLength);var h=c.element.prop("disabled");h===b&&(h=!1),this.enable(!h);var i=c.element.prop("readonly");i===b&&(i=!1),this.readonly(i),k=k||n(),this.autofocus=c.element.prop("autofocus"),c.element.prop("autofocus",!1),this.autofocus&&this.focus()},destroy:function(){var a=this.opts.element.data("select2");this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),a!==b&&(a.container.remove(),a.dropdown.remove(),a.opts.element.removeClass("select2-offscreen").removeData("select2").off(".select2").attr({tabindex:this.elementTabIndex}).prop("autofocus",this.autofocus||!1).show())},optionToData:function(a){return a.is("option")?{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:o(a.attr("locked"),"locked")}:a.is("optgroup")?{text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")}:b},prepareOpts:function(c){var d,e,f,g,h=this;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw Error("Option '"+this+"' is not allowed for Select2 when attached to a <select> element.")}),c=a.extend({},{populateResults:function(d,e,f){var g,l=this.opts.id;g=function(d,e,i){var j,k,m,n,o,p,q,r,s,t;for(d=c.sortResults(d,e,f),j=0,k=d.length;k>j;j+=1)m=d[j],o=m.disabled===!0,n=!o&&l(m)!==b,p=m.children&&m.children.length>0,q=a("<li></li>"),q.addClass("select2-results-dept-"+i),q.addClass("select2-result"),q.addClass(n?"select2-result-selectable":"select2-result-unselectable"),o&&q.addClass("select2-disabled"),p&&q.addClass("select2-result-with-children"),q.addClass(h.opts.formatResultCssClass(m)),r=a(document.createElement("div")),r.addClass("select2-result-label"),t=c.formatResult(m,r,f,h.opts.escapeMarkup),t!==b&&r.html(t),q.append(r),p&&(s=a("<ul></ul>"),s.addClass("select2-result-sub"),g(m.children,s,i+1),q.append(s)),q.data("select2-data",m),e.append(q)},g(e,d,0)}},a.fn.select2.defaults,c),"function"!=typeof c.id&&(f=c.id,c.id=function(a){return a[f]}),a.isArray(c.element.data("select2Tags"))){if("tags"in c)throw"tags specified as both an attribute 'data-select2-tags' and in options of Select2 "+c.element.attr("id");c.tags=c.element.data("select2Tags")}if(e?(c.query=this.bind(function(c){var g,i,j,e={results:[],more:!1},f=c.term;j=function(a,b){var d;a.is("option")?c.matcher(f,a.text(),a)&&b.push(h.optionToData(a)):a.is("optgroup")&&(d=h.optionToData(a),a.children().each2(function(a,b){j(b,d.children)}),d.children.length>0&&b.push(d))},g=d.children(),this.getPlaceholder()!==b&&g.length>0&&(i=g[0],""===a(i).text()&&(g=g.not(i))),g.each2(function(a,b){j(b,e.results)}),c.callback(e)}),c.id=function(a){return a.id},c.formatResultCssClass=function(a){return a.css}):"query"in c||("ajax"in c?(g=c.element.data("ajax-url"),g&&g.length>0&&(c.ajax.url=g),c.query=D.call(c.element,c.ajax)):"data"in c?c.query=E(c.data):"tags"in c&&(c.query=F(c.tags),c.createSearchChoice===b&&(c.createSearchChoice=function(a){return{id:a,text:a}}),c.initSelection===b&&(c.initSelection=function(d,e){var f=[];a(p(d.val(),c.separator)).each(function(){var d=this,e=this,g=c.tags;a.isFunction(g)&&(g=g()),a(g).each(function(){return o(this.id,d)?(e=this.text,!1):b}),f.push({id:d,text:e})}),e(f)}))),"function"!=typeof c.query)throw"query function not defined for Select2 "+c.element.attr("id");return c},monitorSource:function(){var c,a=this.opts.element;a.on("change.select2",this.bind(function(){this.opts.element.data("select2-change-triggered")!==!0&&this.initSelection()})),c=this.bind(function(){var d,f=a.prop("disabled");f===b&&(f=!1),this.enable(!f);var d=a.prop("readonly");d===b&&(d=!1),this.readonly(d),B(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.addClass(H(this.opts.containerCssClass)),B(this.dropdown,this.opts.element,this.opts.adaptDropdownCssClass),this.dropdown.addClass(H(this.opts.dropdownCssClass))}),a.on("propertychange.select2 DOMAttrModified.select2",c),this.mutationCallback===b&&(this.mutationCallback=function(a){a.forEach(c)}),"undefined"!=typeof WebKitMutationObserver&&(this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),this.propertyObserver=new WebKitMutationObserver(this.mutationCallback),this.propertyObserver.observe(a.get(0),{attributes:!0,subtree:!1}))},triggerSelect:function(b){var c=a.Event("select2-selecting",{val:this.id(b),object:b});return this.opts.element.trigger(c),!c.isDefaultPrevented()},triggerChange:function(b){b=b||{},b=a.extend({},b,{type:"change",val:this.val()}),this.opts.element.data("select2-change-triggered",!0),this.opts.element.trigger(b),this.opts.element.data("select2-change-triggered",!1),this.opts.element.click(),this.opts.blurOnChange&&this.opts.element.blur()},isInterfaceEnabled:function(){return this.enabledInterface===!0},enableInterface:function(){var a=this._enabled&&!this._readonly,b=!a;return a===this.enabledInterface?!1:(this.container.toggleClass("select2-container-disabled",b),this.close(),this.enabledInterface=a,!0)},enable:function(a){return a===b&&(a=!0),this._enabled===a?!1:(this._enabled=a,this.opts.element.prop("disabled",!a),this.enableInterface(),!0)},readonly:function(a){return a===b&&(a=!1),this._readonly===a?!1:(this._readonly=a,this.opts.element.prop("readonly",a),this.enableInterface(),!0)},opened:function(){return this.container.hasClass("select2-dropdown-open")},positionDropdown:function(){var q,r,s,t,b=this.dropdown,c=this.container.offset(),d=this.container.outerHeight(!1),e=this.container.outerWidth(!1),f=b.outerHeight(!1),g=a(window).scrollLeft()+a(window).width(),h=a(window).scrollTop()+a(window).height(),i=c.top+d,j=c.left,l=h>=i+f,m=c.top-f>=this.body().scrollTop(),n=b.outerWidth(!1),o=g>=j+n,p=b.hasClass("select2-drop-above");this.opts.dropdownAutoWidth?(t=a(".select2-results",b)[0],b.addClass("select2-drop-auto-width"),b.css("width",""),n=b.outerWidth(!1)+(t.scrollHeight===t.clientHeight?0:k.width),n>e?e=n:n=e,o=g>=j+n):this.container.removeClass("select2-drop-auto-width"),"static"!==this.body().css("position")&&(q=this.body().offset(),i-=q.top,j-=q.left),p?(r=!0,!m&&l&&(r=!1)):(r=!1,!l&&m&&(r=!0)),o||(j=c.left+e-n),r?(i=c.top-f,this.container.addClass("select2-drop-above"),b.addClass("select2-drop-above")):(this.container.removeClass("select2-drop-above"),b.removeClass("select2-drop-above")),s=a.extend({top:i,left:j,width:e},H(this.opts.dropdownCss)),b.css(s)},shouldOpen:function(){var b;return this.opened()?!1:this._enabled===!1||this._readonly===!0?!1:(b=a.Event("select2-opening"),this.opts.element.trigger(b),!b.isDefaultPrevented())},clearDropdownAlignmentPreference:function(){this.container.removeClass("select2-drop-above"),this.dropdown.removeClass("select2-drop-above")},open:function(){return this.shouldOpen()?(this.opening(),!0):!1},opening:function(){function h(){return{width:Math.max(document.documentElement.scrollWidth,a(window).width()),height:Math.max(document.documentElement.scrollHeight,a(window).height())}}var f,b=this.containerId,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.addClass("select2-dropdown-open").addClass("select2-container-active"),this.clearDropdownAlignmentPreference(),this.dropdown[0]!==this.body().children().last()[0]&&this.dropdown.detach().appendTo(this.body()),f=a("#select2-drop-mask"),0==f.length&&(f=a(document.createElement("div")),f.attr("id","select2-drop-mask").attr("class","select2-drop-mask"),f.hide(),f.appendTo(this.body()),f.on("mousedown touchstart",function(b){var d,c=a("#select2-drop");c.length>0&&(d=c.data("select2"),d.opts.selectOnBlur&&d.selectHighlighted({noFocus:!0}),d.close(),b.preventDefault(),b.stopPropagation())})),this.dropdown.prev()[0]!==f[0]&&this.dropdown.before(f),a("#select2-drop").removeAttr("id"),this.dropdown.attr("id","select2-drop"),f.css(h()),f.show(),this.dropdown.show(),this.positionDropdown(),this.dropdown.addClass("select2-drop-active"),this.ensureHighlightVisible();var g=this;this.container.parents().add(window).each(function(){a(this).on(d+" "+c+" "+e,function(){a("#select2-drop-mask").css(h()),g.positionDropdown()})})},close:function(){if(this.opened()){var b=this.containerId,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.parents().add(window).each(function(){a(this).off(c).off(d).off(e)}),this.clearDropdownAlignmentPreference(),a("#select2-drop-mask").hide(),this.dropdown.removeAttr("id"),this.dropdown.hide(),this.container.removeClass("select2-dropdown-open"),this.results.empty(),this.clearSearch(),this.search.removeClass("select2-active"),this.opts.element.trigger(a.Event("select2-close"))}},clearSearch:function(){},getMaximumSelectionSize:function(){return H(this.opts.maximumSelectionSize)},ensureHighlightVisible:function(){var d,e,f,g,h,i,j,c=this.results;if(e=this.highlight(),!(0>e)){if(0==e)return c.scrollTop(0),b;d=this.findHighlightableChoices().find(".select2-result-label"),f=a(d[e]),g=f.offset().top+f.outerHeight(!0),e===d.length-1&&(j=c.find("li.select2-more-results"),j.length>0&&(g=j.offset().top+j.outerHeight(!0))),h=c.offset().top+c.outerHeight(!0),g>h&&c.scrollTop(c.scrollTop()+(g-h)),i=f.offset().top-c.offset().top,0>i&&"none"!=f.css("display")&&c.scrollTop(c.scrollTop()+i)}},findHighlightableChoices:function(){return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)")},moveHighlight:function(b){for(var c=this.findHighlightableChoices(),d=this.highlight();d>-1&&c.length>d;){d+=b;var e=a(c[d]);if(e.hasClass("select2-result-selectable")&&!e.hasClass("select2-disabled")&&!e.hasClass("select2-selected")){this.highlight(d);break}}},highlight:function(c){var e,f,d=this.findHighlightableChoices();return 0===arguments.length?m(d.filter(".select2-highlighted")[0],d.get()):(c>=d.length&&(c=d.length-1),0>c&&(c=0),this.results.find(".select2-highlighted").removeClass("select2-highlighted"),e=a(d[c]),e.addClass("select2-highlighted"),this.ensureHighlightVisible(),f=e.data("select2-data"),f&&this.opts.element.trigger({type:"select2-highlight",val:this.id(f),choice:f}),b)},countSelectableResults:function(){return this.findHighlightableChoices().length},highlightUnderEvent:function(b){var c=a(b.target).closest(".select2-result-selectable");if(c.length>0&&!c.is(".select2-highlighted")){var d=this.findHighlightableChoices();this.highlight(d.index(c))}else 0==c.length&&this.results.find(".select2-highlighted").removeClass("select2-highlighted")},loadMoreIfNeeded:function(){var c,a=this.results,b=a.find("li.select2-more-results"),e=this.resultsPage+1,f=this,g=this.search.val(),h=this.context;0!==b.length&&(c=b.offset().top-a.offset().top-a.height(),this.opts.loadMorePadding>=c&&(b.addClass("select2-active"),this.opts.query({element:this.opts.element,term:g,page:e,context:h,matcher:this.opts.matcher,callback:this.bind(function(c){f.opened()&&(f.opts.populateResults.call(this,a,c.results,{term:g,page:e,context:h}),f.postprocessResults(c,!1,!1),c.more===!0?(b.detach().appendTo(a).text(f.opts.formatLoadMore(e+1)),window.setTimeout(function(){f.loadMoreIfNeeded()},10)):b.remove(),f.positionDropdown(),f.resultsPage=e,f.context=c.context)})})))},tokenize:function(){},updateResults:function(c){function l(){e.scrollTop(0),d.removeClass("select2-active"),h.positionDropdown()}function m(a){e.html(a),l()}var g,i,d=this.search,e=this.results,f=this.opts,h=this,j=d.val(),k=a.data(this.container,"select2-last-term");if((c===!0||!k||!o(j,k))&&(a.data(this.container,"select2-last-term",j),c===!0||this.showSearchInput!==!1&&this.opened())){var n=this.getMaximumSelectionSize();if(n>=1&&(g=this.data(),a.isArray(g)&&g.length>=n&&G(f.formatSelectionTooBig,"formatSelectionTooBig")))return m("<li class='select2-selection-limit'>"+f.formatSelectionTooBig(n)+"</li>"),b;if(d.val().length<f.minimumInputLength)return G(f.formatInputTooShort,"formatInputTooShort")?m("<li class='select2-no-results'>"+f.formatInputTooShort(d.val(),f.minimumInputLength)+"</li>"):m(""),c&&this.showSearch(!0),b;if(f.maximumInputLength&&d.val().length>f.maximumInputLength)return G(f.formatInputTooLong,"formatInputTooLong")?m("<li class='select2-no-results'>"+f.formatInputTooLong(d.val(),f.maximumInputLength)+"</li>"):m(""),b;f.formatSearching&&0===this.findHighlightableChoices().length&&m("<li class='select2-searching'>"+f.formatSearching()+"</li>"),d.addClass("select2-active"),i=this.tokenize(),i!=b&&null!=i&&d.val(i),this.resultsPage=1,f.query({element:f.element,term:d.val(),page:this.resultsPage,context:null,matcher:f.matcher,callback:this.bind(function(g){var i;return this.opened()?(this.context=g.context===b?null:g.context,this.opts.createSearchChoice&&""!==d.val()&&(i=this.opts.createSearchChoice.call(null,d.val(),g.results),i!==b&&null!==i&&h.id(i)!==b&&null!==h.id(i)&&0===a(g.results).filter(function(){return o(h.id(this),h.id(i))}).length&&g.results.unshift(i)),0===g.results.length&&G(f.formatNoMatches,"formatNoMatches")?(m("<li class='select2-no-results'>"+f.formatNoMatches(d.val())+"</li>"),b):(e.empty(),h.opts.populateResults.call(this,e,g.results,{term:d.val(),page:this.resultsPage,context:null}),g.more===!0&&G(f.formatLoadMore,"formatLoadMore")&&(e.append("<li class='select2-more-results'>"+h.opts.escapeMarkup(f.formatLoadMore(this.resultsPage))+"</li>"),window.setTimeout(function(){h.loadMoreIfNeeded()},10)),this.postprocessResults(g,c),l(),this.opts.element.trigger({type:"select2-loaded",data:g}),b)):(this.search.removeClass("select2-active"),b)})})}},cancel:function(){this.close()},blur:function(){this.opts.selectOnBlur&&this.selectHighlighted({noFocus:!0}),this.close(),this.container.removeClass("select2-container-active"),this.search[0]===document.activeElement&&this.search.blur(),this.clearSearch(),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus")},focusSearch:function(){w(this.search)},selectHighlighted:function(a){var b=this.highlight(),c=this.results.find(".select2-highlighted"),d=c.closest(".select2-result").data("select2-data");d&&(this.highlight(b),this.onSelect(d,a))},getPlaceholder:function(){return this.opts.element.attr("placeholder")||this.opts.element.attr("data-placeholder")||this.opts.element.data("placeholder")||this.opts.placeholder},initContainerWidth:function(){function c(){var c,d,e,f,g;if("off"===this.opts.width)return null;if("element"===this.opts.width)return 0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px";if("copy"===this.opts.width||"resolve"===this.opts.width){if(c=this.opts.element.attr("style"),c!==b)for(d=c.split(";"),f=0,g=d.length;g>f;f+=1)if(e=d[f].replace(/\s/g,"").match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i),null!==e&&e.length>=1)return e[1];return c=this.opts.element.css("width"),c&&c.length>0?c:"resolve"===this.opts.width?0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px":null}return a.isFunction(this.opts.width)?this.opts.width():this.opts.width}var d=c.call(this);null!==d&&this.container.css("width",d)}}),e=K(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container"}).html(["<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>"," <span> </span><abbr class='select2-search-choice-close'></abbr>"," <div><b></b></div>","</a>","<input class='select2-focusser select2-offscreen' type='text'/>","<div class='select2-drop select2-display-none'>"," <div class='select2-search'>"," <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'/>"," </div>"," <ul class='select2-results'>"," </ul>","</div>"].join(""));return b},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var b,c;this.parent.opening.apply(this,arguments),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.search.focus(),b=this.search.get(0),b.createTextRange&&(c=b.createTextRange(),c.collapse(!1),c.select()),this.focusser.prop("disabled",!0).val(""),this.updateResults(!0),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.removeAttr("disabled"),this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus()},initContainer:function(){var d,e=this.container,f=this.dropdown;this.showSearch(!1),this.selection=d=e.find(".select2-choice"),this.focusser=e.find(".select2-focusser"),this.focusser.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.focusser.attr("id")),this.focusser.attr("tabindex",this.elementTabIndex),this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){if(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)return y(a),b;switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),b;case c.ESC:return this.cancel(a),y(a),b}}})),this.search.on("blur",this.bind(function(){document.activeElement===this.body().get(0)&&window.setTimeout(this.bind(function(){this.search.focus()}),0)})),this.focusser.on("keydown",this.bind(function(a){return!this.isInterfaceEnabled()||a.which===c.TAB||c.isControl(a)||c.isFunctionKey(a)||a.which===c.ESC?b:this.opts.openOnEnter===!1&&a.which===c.ENTER?(y(a),b):a.which==c.DOWN||a.which==c.UP||a.which==c.ENTER&&this.opts.openOnEnter?(this.open(),y(a),b):a.which==c.DELETE||a.which==c.BACKSPACE?(this.opts.allowClear&&this.clear(),y(a),b):b})),r(this.focusser),this.focusser.on("keyup-change input",this.bind(function(a){a.stopPropagation(),this.opened()||this.open()})),d.on("mousedown","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),z(a),this.close(),this.selection.focus())})),d.on("mousedown",this.bind(function(b){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.opened()?this.close():this.isInterfaceEnabled()&&this.open(),y(b)})),f.on("mousedown",this.bind(function(){this.search.focus()})),d.on("focus",this.bind(function(a){y(a)})),this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(a.Event("select2-blur")))})),this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.setPlaceholder()},clear:function(a){var b=this.selection.data("select2-data");b&&(this.opts.element.val(""),this.selection.find("span").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),a!==!1&&(this.opts.element.trigger({type:"select2-removed",val:this.id(b),choice:b}),this.triggerChange({removed:b})))},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text())this.updateSelection([]),this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder())})}},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=a.find(":selected");b(c.optionToData(d))}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=o(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&""!==this.select.find("option").first().text()?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(""===this.opts.element.val()&&a!==b){if(this.select&&""!==this.select.find("option:first").text())return;this.selection.find("span").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear")}},postprocessResults:function(a,c,d){var e=0,f=this;if(this.findHighlightableChoices().each2(function(a,c){return o(f.id(c.data("select2-data")),f.opts.element.val())?(e=a,!1):b}),d!==!1&&this.highlight(e),c===!0&&this.showSearchInput===!1){var h=this.opts.minimumResultsForSearch;h>=0&&this.showSearch(I(a.results)>=h)}},showSearch:function(b){this.showSearchInput=b,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!b),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!b),a(this.dropdown,this.container).toggleClass("select2-with-searchbox",b)},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(),d=this.data();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a}),this.close(),b&&b.noFocus||this.selection.focus(),o(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var d,c=this.selection.find("span");this.selection.data("select2-data",a),c.empty(),d=this.opts.formatSelection(a,c),d!==b&&c.append(this.opts.escapeMarkup(d)),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.container.addClass("select2-allowclear")},val:function(){var a,c=!1,d=null,e=this,f=this.data();if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find(":selected").each2(function(a,b){return d=e.optionToData(b),!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange({added:d,removed:f});else{if(this.opts.initSelection===b)throw Error("cannot call val() if initSelection() is not defined");if(!a&&0!==a)return this.clear(c),b;this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange({added:a,removed:f})})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a,c){var d;return 0===arguments.length?(d=this.selection.data("select2-data"),d==b&&(d=null),d):(a&&""!==a?(d=this.data(),this.opts.element.val(a?this.id(a):""),this.updateSelection(a),c&&this.triggerChange({added:a,removed:d})):this.clear(c),b)}}),f=K(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi"}).html([" <ul class='select2-choices'>"," <li class='select2-search-field'>"," <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'>"," </li>","</ul>","<div class='select2-drop select2-drop-multi select2-display-none'>"," <ul class='select2-results'>"," </ul>","</div>"].join("")); +return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=[];a.find(":selected").each2(function(a,b){d.push(c.optionToData(b))}),b(d)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=p(c.val(),b.separator),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return o(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){for(var a=[],c=0;e.length>c;c++)for(var g=e[c],h=0;f.length>h;h++){var i=f[h];if(o(g,b.id(i))){a.push(i),f.splice(h,1);break}}d(a)}:a.noop})}),b},selectChoice:function(a){var b=this.container.find(".select2-search-choice-focus");b.length&&a&&a[0]==b[0]||(b.length&&this.opts.element.trigger("choice-deselected",b),b.removeClass("select2-search-choice-focus"),a&&a.length&&(this.close(),a.addClass("select2-search-choice-focus"),this.opts.element.trigger("choice-selected",a)))},initContainer:function(){var e,d=".select2-choices";this.searchContainer=this.container.find(".select2-search-field"),this.selection=e=this.container.find(d);var f=this;this.selection.on("mousedown",".select2-search-choice",function(){f.search[0].focus(),f.selectChoice(a(this))}),this.search.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.search.attr("id")),this.search.on("input paste",this.bind(function(){this.isInterfaceEnabled()&&(this.opened()||this.open())})),this.search.attr("tabindex",this.elementTabIndex),this.keydowns=0,this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){++this.keydowns;var d=e.find(".select2-search-choice-focus"),f=d.prev(".select2-search-choice:not(.select2-locked)"),g=d.next(".select2-search-choice:not(.select2-locked)"),h=x(this.search);if(d.length&&(a.which==c.LEFT||a.which==c.RIGHT||a.which==c.BACKSPACE||a.which==c.DELETE||a.which==c.ENTER)){var i=d;return a.which==c.LEFT&&f.length?i=f:a.which==c.RIGHT?i=g.length?g:null:a.which===c.BACKSPACE?(this.unselect(d.first()),this.search.width(10),i=f.length?f:g):a.which==c.DELETE?(this.unselect(d.first()),this.search.width(10),i=g.length?g:null):a.which==c.ENTER&&(i=null),this.selectChoice(i),y(a),i&&i.length||this.open(),b}if((a.which===c.BACKSPACE&&1==this.keydowns||a.which==c.LEFT)&&0==h.offset&&!h.length)return this.selectChoice(e.find(".select2-search-choice:not(.select2-locked)").last()),y(a),b;if(this.selectChoice(null),this.opened())switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),b;case c.ESC:return this.cancel(a),y(a),b}if(a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.BACKSPACE&&a.which!==c.ESC){if(a.which===c.ENTER){if(this.opts.openOnEnter===!1)return;if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return}this.open(),(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)&&y(a),a.which===c.ENTER&&y(a)}}})),this.search.on("keyup",this.bind(function(){this.keydowns=0,this.resizeSearch()})),this.search.on("blur",this.bind(function(b){this.container.removeClass("select2-container-active"),this.search.removeClass("select2-focused"),this.selectChoice(null),this.opened()||this.clearSearch(),b.stopImmediatePropagation(),this.opts.element.trigger(a.Event("select2-blur"))})),this.container.on("mousedown",d,this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").length>0||(this.selectChoice(null),this.clearPlaceholder(),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.open(),this.focusSearch(),b.preventDefault()))})),this.container.on("focus",d,this.bind(function(){this.isInterfaceEnabled()&&(this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.clearSearch()},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.search.prop("disabled",!this.isInterfaceEnabled())},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder(),c=this.getMaxSearchWidth();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(c>0?c:this.container.css("width"))):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),this.updateResults(!0),this.search.focus(),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){0>m(e.id(this),c)&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer(a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,b){this.triggerSelect(a)&&(this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()&&this.updateResults(!0),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),b&&b.noFocus||this.focusSearch())},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,d=!c.locked,e=a("<li class='select2-search-choice'> <div></div> <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a></li>"),f=a("<li class='select2-search-choice select2-locked'><div></div></li>"),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div")),j!=b&&g.find("div").replaceWith("<div title='"+this.opts.escapeMarkup(j)+"'>"+this.opts.escapeMarkup(j)+"</div>"),d&&g.find(".select2-search-choice-close").on("mousedown",y).on("click dblclick",this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").fadeOut("fast",this.bind(function(){this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),this.close(),this.focusSearch()})).dequeue(),y(b))})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(a){var c,d,b=this.getVal();if(a=a.closest(".select2-search-choice"),0===a.length)throw"Invalid argument: "+a+". Must be .select2-search-choice";c=a.data("select2-data"),c&&(d=m(this.id(c),b),d>=0&&(b.splice(d,1),this.setVal(b),this.select&&this.postprocessResults()),a.remove(),this.opts.element.trigger({type:"removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))},postprocessResults:function(a,b,c){var d=this.getVal(),e=this.results.find(".select2-result"),f=this.results.find(".select2-result-with-children"),g=this;e.each2(function(a,b){var c=g.id(b.data("select2-data"));m(c,d)>=0&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))}),f.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&c!==!1&&g.highlight(0),!this.opts.createSearchChoice&&!e.filter(".select2-result:not(.select2-selected)").length>0&&this.results.append("<li class='select2-no-results'>"+g.opts.formatNoMatches(g.search.val())+"</li>")},getMaxSearchWidth:function(){return this.selection.width()-q(this.search)},resizeSearch:function(){var a,b,c,d,e,f=q(this.search);a=A(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(e)},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),p(a,this.opts.separator))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){0>m(this,c)&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},buildChangeDetails:function(a,b){for(var b=b.slice(0),a=a.slice(0),c=0;b.length>c;c++)for(var d=0;a.length>d;d++)o(this.opts.id(b[c]),this.opts.id(a[d]))&&(b.splice(c,1),c--,a.splice(d,1),d--);return{added:b,removed:a}},val:function(c,d){var e,f=this;if(0===arguments.length)return this.getVal();if(e=this.data(),e.length||(e=[]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange({added:this.data(),removed:e}),b;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange(this.buildChangeDetails(e,this.data()));else{if(this.opts.initSelection===b)throw Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a(b).map(f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange(this.buildChangeDetails(e,this.data()))})}this.clearSearch()},onSortStart:function(){if(this.select)throw Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(c,d){var f,g,e=this;return 0===arguments.length?this.selection.find(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(g=this.data(),c||(c=[]),f=a.map(c,function(a){return e.opts.id(a)}),this.setVal(f),this.updateSelection(c),this.clearSearch(),d&&this.triggerChange(this.buildChangeDetails(g,this.data())),b)}}),a.fn.select2=function(){var d,g,h,i,c=Array.prototype.slice.call(arguments,0),j=["val","destroy","opened","open","close","focus","isFocused","container","onSortStart","onSortEnd","enable","readonly","positionDropdown","data"],k=["val","opened","isFocused","container","data"];return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?i=d.element.prop("multiple"):(i=d.multiple||!1,"tags"in d&&(d.multiple=i=!0)),g=i?new f:new e,g.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(0>m(c[0],j))throw"Unknown method: "+c[0];if(h=b,g=a(this).data("select2"),g===b)return;if(h="container"===c[0]?g.container:g[c[0]].apply(g,c.slice(1)),m(c[0],k)>=0)return!1}}),h===b?this:h},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return C(a.text,c.term,e,d),e.join("")},formatSelection:function(a){return a?a.text:b},sortResults:function(a){return a},formatResultCssClass:function(){return b},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results..."},formatSearching:function(){return"Searching..."},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a.id},matcher:function(a,b){return(""+b).toUpperCase().indexOf((""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:J,escapeMarkup:function(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return(a+"").replace(/[&<>"'\/\\]/g,function(a){return b[a]})},blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null}},a.fn.select2.ajaxDefaults={transport:a.ajax,params:{type:"GET",cache:!1,dataType:"json"}},window.Select2={query:{ajax:D,local:E,tags:F},util:{debounce:t,markMatch:C},"class":{"abstract":d,single:e,multi:f}}}}(jQuery); \ No newline at end of file diff --git a/src/inputs/select2/lib/select2_locale_de.js b/src/inputs/select2/lib/select2_locale_de.js deleted file mode 100644 index 01f94ed..0000000 --- a/src/inputs/select2/lib/select2_locale_de.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 German translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Keine Übereinstimmungen gefunden"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Bitte " + n + " Zeichen mehr eingeben"; }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Bitte " + n + " Zeichen weniger eingeben"; }, - formatSelectionTooBig: function (limit) { return "Sie können nur " + limit + " Eintr" + (limit === 1 ? "ag" : "äge") + " auswählen"; }, - formatLoadMore: function (pageNumber) { return "Lade mehr Ergebnisse..."; }, - formatSearching: function () { return "Suche..."; } - }); -})(jQuery); \ No newline at end of file diff --git a/src/inputs/select2/lib/select2_locale_en.js.template b/src/inputs/select2/lib/select2_locale_en.js.template deleted file mode 100644 index 260c6c4..0000000 --- a/src/inputs/select2/lib/select2_locale_en.js.template +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Select2 <Language> translation. - * - * Author: Your Name <your@email> - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "No matches found"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1 ? "" : "s"); }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Please enter " + n + " less character" + (n == 1? "" : "s"); }, - formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); }, - formatLoadMore: function (pageNumber) { return "Loading more results..."; }, - formatSearching: function () { return "Searching..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_es.js b/src/inputs/select2/lib/select2_locale_es.js deleted file mode 100644 index 1b0a021..0000000 --- a/src/inputs/select2/lib/select2_locale_es.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 Spanish translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "No se encontraron resultados"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Por favor adicione " + n + " caracter" + (n == 1? "" : "es"); }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Por favor elimine " + n + " caracter" + (n == 1? "" : "es"); }, - formatSelectionTooBig: function (limit) { return "Solo puede seleccionar " + limit + " elemento" + (limit == 1 ? "" : "s"); }, - formatLoadMore: function (pageNumber) { return "Cargando más resultados..."; }, - formatSearching: function () { return "Buscando..."; } - }); -})(jQuery); \ No newline at end of file diff --git a/src/inputs/select2/lib/select2_locale_eu.js b/src/inputs/select2/lib/select2_locale_eu.js deleted file mode 100644 index 05665f5..0000000 --- a/src/inputs/select2/lib/select2_locale_eu.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Select2 Basque translation. - * - * Author: Julen Ruiz Aizpuru <julenx at gmail dot com> - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { - return "Ez da bat datorrenik aurkitu"; - }, - formatInputTooShort: function (input, min) { - var n = min - input.length; - if (n === 1) { - return "Idatzi karaktere bat gehiago"; - } else { - return "Idatzi " + n + " karaktere gehiago"; - } - }, - formatInputTooLong: function (input, max) { - var n = input.length - max; - if (n === 1) { - return "Idatzi karaktere bat gutxiago"; - } else { - return "Idatzi " + n + " karaktere gutxiago"; - } - }, - formatSelectionTooBig: function (limit) { - if (limit === 1 ) { - return "Elementu bakarra hauta dezakezu"; - } else { - return limit + " elementu hauta ditzakezu soilik"; - } - }, - formatLoadMore: function (pageNumber) { - return "Emaitza gehiago kargatzen..."; - }, - formatSearching: function () { - return "Bilatzen..."; - } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_fr.js b/src/inputs/select2/lib/select2_locale_fr.js deleted file mode 100644 index d369332..0000000 --- a/src/inputs/select2/lib/select2_locale_fr.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 French translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Aucun résultat trouvé"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Merci de saisir " + n + " caractère" + (n == 1? "" : "s") + " de plus"; }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Merci de saisir " + n + " caractère" + (n == 1? "" : "s") + " de moins"; }, - formatSelectionTooBig: function (limit) { return "Vous pouvez seulement sélectionner " + limit + " élément" + (limit == 1 ? "" : "s"); }, - formatLoadMore: function (pageNumber) { return "Chargement de résultats supplémentaires..."; }, - formatSearching: function () { return "Recherche en cours..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_hr.js b/src/inputs/select2/lib/select2_locale_hr.js deleted file mode 100644 index b061540..0000000 --- a/src/inputs/select2/lib/select2_locale_hr.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Select2 Croatian translation. - * - * Author: Edi Modrić <edi.modric@gmail.com> - */ -(function ($) { - "use strict"; - - var specialNumbers = { - 1: function(n) { return (n % 100 != 11 ? "znak" : "znakova"); }, - 2: function(n) { return (n % 100 != 12 ? "znaka" : "znakova"); }, - 3: function(n) { return (n % 100 != 13 ? "znaka" : "znakova"); }, - 4: function(n) { return (n % 100 != 14 ? "znaka" : "znakova"); } - }; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Nema rezultata"; }, - formatInputTooShort: function (input, min) { - var n = min - input.length; - var nMod10 = n % 10; - - if (nMod10 > 0 && nMod10 < 5) { - return "Unesite još " + n + " " + specialNumbers[nMod10](n); - } - - return "Unesite još " + n + " znakova"; - }, - formatInputTooLong: function (input, max) { - var n = input.length - max; - var nMod10 = n % 10; - - if (nMod10 > 0 && nMod10 < 5) { - return "Unesite " + n + " " + specialNumbers[nMod10](n) + " manje"; - } - - return "Unesite " + n + " znakova manje"; - }, - formatSelectionTooBig: function (limit) { return "Maksimalan broj odabranih stavki je " + limit; }, - formatLoadMore: function (pageNumber) { return "Učitavanje rezultata..."; }, - formatSearching: function () { return "Pretraga..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_hu.js b/src/inputs/select2/lib/select2_locale_hu.js deleted file mode 100644 index 572dea9..0000000 --- a/src/inputs/select2/lib/select2_locale_hu.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 Hungarian translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Nincs találat."; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Túl rövid. Még " + n + " karakter hiányzik."; }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Túl hosszú. " + n + " kerekterrel több mint kellene."; }, - formatSelectionTooBig: function (limit) { return "Csak " + limit + " elemet lehet kiválasztani."; }, - formatLoadMore: function (pageNumber) { return "Töltés..."; }, - formatSearching: function () { return "Keresés..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_it.js b/src/inputs/select2/lib/select2_locale_it.js deleted file mode 100644 index 98369dd..0000000 --- a/src/inputs/select2/lib/select2_locale_it.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 Italian translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Nessuna corrispondenza trovata"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Inserisci ancora " + n + " caratter" + (n == 1? "e" : "i"); }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Inserisci " + n + " caratter" + (n == 1? "e" : "i") + " in meno"; }, - formatSelectionTooBig: function (limit) { return "Puoi selezionare solo " + limit + " element" + (limit == 1 ? "o" : "i"); }, - formatLoadMore: function (pageNumber) { return "Caricamento in corso..."; }, - formatSearching: function () { return "Ricerca..."; } - }); -})(jQuery); \ No newline at end of file diff --git a/src/inputs/select2/lib/select2_locale_nl.js b/src/inputs/select2/lib/select2_locale_nl.js deleted file mode 100644 index 2ee2662..0000000 --- a/src/inputs/select2/lib/select2_locale_nl.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 Dutch translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Geen resultaten gevonden"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Vul " + n + " karakter" + (n == 1? "" : "s") + " meer in"; }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Vul " + n + " karakter" + (n == 1? "" : "s") + " minder in"; }, - formatSelectionTooBig: function (limit) { return "Maximaal " + limit + " item" + (limit == 1 ? "" : "s") + " toegestaan"; }, - formatLoadMore: function (pageNumber) { return "Meer resultaten laden..."; }, - formatSearching: function () { return "Zoeken..."; }, - }); -})(jQuery); \ No newline at end of file diff --git a/src/inputs/select2/lib/select2_locale_pt-BR.js b/src/inputs/select2/lib/select2_locale_pt-BR.js deleted file mode 100644 index 701fb7f..0000000 --- a/src/inputs/select2/lib/select2_locale_pt-BR.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 Brazilian Portuguese translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Nenhum resultado encontrado"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Informe " + n + " caracter" + (n == 1? "" : "es"); }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Apague " + n + " caracter" + (n == 1? "" : "es"); }, - formatSelectionTooBig: function (limit) { return "Só é possível selecionar " + limit + " elemento" + (limit == 1 ? "" : "s"); }, - formatLoadMore: function (pageNumber) { return "Carregando mais resultados..."; }, - formatSearching: function () { return "Buscando..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_pt-PT.js b/src/inputs/select2/lib/select2_locale_pt-PT.js deleted file mode 100644 index 008653e..0000000 --- a/src/inputs/select2/lib/select2_locale_pt-PT.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 Portuguese (Portugal) translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Nenhum resultado encontrado"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Introduza " + n + " caracter" + (n == 1 ? "" : "es"); }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Apague " + n + " caracter" + (n == 1 ? "" : "es"); }, - formatSelectionTooBig: function (limit) { return "Só é possível selecionar " + limit + " elemento" + (limit == 1 ? "" : "s"); }, - formatLoadMore: function (pageNumber) { return "A carregar mais resultados..."; }, - formatSearching: function () { return "A pesquisar..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_ro.js b/src/inputs/select2/lib/select2_locale_ro.js deleted file mode 100644 index 88b3ac4..0000000 --- a/src/inputs/select2/lib/select2_locale_ro.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 Romanian translation. - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Nu a fost găsit nimic"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Vă rugăm să introduceți incă " + n + " caracter" + (n == 1 ? "" : "e"); }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Vă rugăm să introduceți mai puțin de " + n + " caracter" + (n == 1? "" : "e"); }, - formatSelectionTooBig: function (limit) { return "Aveți voie să selectați cel mult " + limit + " element" + (limit == 1 ? "" : "e"); }, - formatLoadMore: function (pageNumber) { return "Se încarcă..."; }, - formatSearching: function () { return "Căutare..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_ru.js b/src/inputs/select2/lib/select2_locale_ru.js deleted file mode 100644 index f5df5f1..0000000 --- a/src/inputs/select2/lib/select2_locale_ru.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Select2 Russian translation - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Совпадений не найдено"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Пожалуйста, введите еще " + n + " символ" + (n == 1 ? "" : ((n > 1)&&(n < 5) ? "а" : "ов")); }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Пожалуйста, введите на " + n + " символ" + (n == 1 ? "" : ((n > 1)&&(n < 5)? "а" : "ов")) + " меньше"; }, - formatSelectionTooBig: function (limit) { return "Вы можете выбрать не более " + limit + " элемент" + (limit == 1 ? "" : ((limit > 1)&&(limit < 5)? "а" : "ов")); }, - formatLoadMore: function (pageNumber) { return "Загрузка данных..."; }, - formatSearching: function () { return "Поиск..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_sk.js b/src/inputs/select2/lib/select2_locale_sk.js deleted file mode 100644 index 8d4e46a..0000000 --- a/src/inputs/select2/lib/select2_locale_sk.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Select2 Slovak translation. - * - * Author: David Vallner <david@vallner.net> - */ -(function ($) { - "use strict"; - // use text for the numbers 2 through 4 - var smallNumbers = { - 2: function(masc) { return (masc ? "dva" : "dve"); }, - 3: function() { return "tri"; }, - 4: function() { return "štyri"; } - } - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Nenašli sa žiadne položky"; }, - formatInputTooShort: function (input, min) { - var n = min - input.length; - if (n == 1) { - return "Prosím zadajte ešte jeden znak"; - } else if (n <= 4) { - return "Prosím zadajte ešte ďalšie "+smallNumbers[n](true)+" znaky"; - } else { - return "Prosím zadajte ešte ďalších "+n+" znakov"; - } - }, - formatInputTooLong: function (input, max) { - var n = input.length - max; - if (n == 1) { - return "Prosím zadajte o jeden znak menej"; - } else if (n <= 4) { - return "Prosím zadajte o "+smallNumbers[n](true)+" znaky menej"; - } else { - return "Prosím zadajte o "+n+" znakov menej"; - } - }, - formatSelectionTooBig: function (limit) { - if (limit == 1) { - return "Môžete zvoliť len jednu položku"; - } else if (limit <= 4) { - return "Môžete zvoliť najviac "+smallNumbers[limit](false)+" položky"; - } else { - return "Môžete zvoliť najviac "+limit+" položiek"; - } - }, - formatLoadMore: function (pageNumber) { return "Načítavajú sa ďalšie výsledky..."; }, - formatSearching: function () { return "Vyhľadávanie..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_sv.js b/src/inputs/select2/lib/select2_locale_sv.js deleted file mode 100644 index 9f09de3..0000000 --- a/src/inputs/select2/lib/select2_locale_sv.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Select2 Swedish translation. - * - * Author: Jens Rantil <jens.rantil@telavox.com> - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Inga träffar"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "Var god skriv in " + n + (n>1 ? " till tecken" : " tecken till"); }, - formatInputTooLong: function (input, max) { var n = input.length - max; return "Var god sudda ut " + n + " tecken"; }, - formatSelectionTooBig: function (limit) { return "Du kan max välja " + limit + " element"; }, - formatLoadMore: function (pageNumber) { return "Laddar fler resultat..."; }, - formatSearching: function () { return "Söker..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_tr.js b/src/inputs/select2/lib/select2_locale_tr.js deleted file mode 100644 index 70b0c76..0000000 --- a/src/inputs/select2/lib/select2_locale_tr.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Select2 Turkish translation. - * - * Author: Salim KAYABAŞI <salim.kayabasi@gmail.com> - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Sonuç bulunamadı"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "En az " + n + " karakter daha girmelisiniz"; }, - formatInputTooLong: function (input, max) { var n = input.length - max; return n + " karakter azaltmalısınız"; }, - formatSelectionTooBig: function (limit) { return "Sadece " + limit + " seçim yapabilirsiniz"; }, - formatLoadMore: function (pageNumber) { return "Daha fazla ..."; }, - formatSearching: function () { return "Aranıyor..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_ua.js b/src/inputs/select2/lib/select2_locale_ua.js deleted file mode 100644 index 58d31e7..0000000 --- a/src/inputs/select2/lib/select2_locale_ua.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Select2 <Language> translation. - * - * Author: bigmihail <bigmihail@bigmir.net> - */ -(function ($) { - "use strict"; - - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "Нічого не знайдено"; }, - formatInputTooShort: function (input, min) { var n = min - input.length, s = ["", "и", "ів"], p = [2,0,1,1,1,2]; return "Введіть буль ласка ще " + n + " символ" + s[ (n%100>4 && n%100<=20)? 2 : p[Math.min(n%10, 5)] ]; }, - formatInputTooLong: function (input, max) { var n = input.length - max, s = ["", "и", "ів"], p = [2,0,1,1,1,2]; return "Введіть буль ласка на " + n + " символ" + s[ (n%100>4 && n%100<=20)? 2 : p[Math.min(n%10, 5)] ] + " менше"; }, - formatSelectionTooBig: function (limit) {var s = ["", "и", "ів"], p = [2,0,1,1,1,2]; return "Ви можете вибрати лише " + limit + " елемент" + s[ (limit%100>4 && limit%100<=20)? 2 : p[Math.min(limit%10, 5)] ]; }, - formatLoadMore: function (pageNumber) { return "Завантаження даних..."; }, - formatSearching: function () { return "Пошук..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/lib/select2_locale_zh-CN.js b/src/inputs/select2/lib/select2_locale_zh-CN.js deleted file mode 100644 index 49d8e59..0000000 --- a/src/inputs/select2/lib/select2_locale_zh-CN.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Select2 Chinese translation - */ -(function ($) { - "use strict"; - $.extend($.fn.select2.defaults, { - formatNoMatches: function () { return "没有找到匹配项"; }, - formatInputTooShort: function (input, min) { var n = min - input.length; return "请再输入" + n + "个字符";}, - formatInputTooLong: function (input, max) { var n = input.length - max; return "请删掉" + n + "个字符";}, - formatSelectionTooBig: function (limit) { return "你只能选择最多" + limit + "项"; }, - formatLoadMore: function (pageNumber) { return "加载结果中..."; }, - formatSearching: function () { return "搜索中..."; } - }); -})(jQuery); diff --git a/src/inputs/select2/select2.js b/src/inputs/select2/select2.js index f68b606..8865c63 100644 --- a/src/inputs/select2/select2.js +++ b/src/inputs/select2/select2.js @@ -1,24 +1,28 @@ /** 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: +Please see [original select2 docs](http://ivaynberg.github.com/select2) for detailed description and options. +Compatible **select2 version is 3.4.1**! +You should manually download and 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): +To 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. +**Note:** currently `autotext` feature does not work for select2 with `ajax` remote source. +You need initially put both `data-value` and element's text youself: + + <a href="#" data-type="select2" data-value="1">Text1</a> + @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> +<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-title="Select country"></a> <script> $(function(){ $('#country').editable({ @@ -39,56 +43,47 @@ $(function(){ var Constructor = function (options) { this.init('select2', options, Constructor.defaults); - + options.select2 = options.select2 || {}; + + this.sourceData = null; - 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); - }; + //placeholder + if(options.placeholder) { + options.select2.placeholder = options.placeholder; } + + //if not `tags` mode, use source + if(!options.select2.tags && options.source) { + var source = options.source; + //if source is function, call it (once!) + if ($.isFunction(options.source)) { + source = options.source.call(options.scope); + } + + if (typeof source === 'string') { + options.select2.ajax = options.select2.ajax || {}; + //some default ajax params + if(!options.select2.ajax.data) { + options.select2.ajax.data = function(term) {return { query:term };}; + } + if(!options.select2.ajax.results) { + options.select2.ajax.results = function(data) { return {results:data };}; + } + options.select2.ajax.url = source; + } else { + //check format and convert x-editable format to select2 format (if needed) + this.sourceData = this.convertSource(source); + options.select2.data = this.sourceData; + } + } //overriding objects in config (as by default jQuery extend() is not recursive) - this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2); + this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2); + + //detect whether it is multi-valued + this.isMultiple = this.options.select2.tags || this.options.select2.multiple; + this.isRemote = ('ajax' in this.options.select2); }; $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput); @@ -96,21 +91,17 @@ $(function(){ $.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); - } - */ + //when data is loaded via ajax, we need to know when it's done to populate listData + if(this.isRemote) { + //listen to loaded event to populate data + this.$input.on('select2-loaded', $.proxy(function(e) { + this.sourceData = e.items.results; + }, this)); } - //trigger resize of editableform to re-position container in multi-valued mode if(this.isMultiple) { @@ -122,20 +113,16 @@ $(function(){ 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(this.options.select2.tags) { //in tags mode just assign value + data = value; + } else if(this.sourceData) { + data = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + } else { + //can not get list of possible values (e.g. autotext for select2 with ajax source) } + //data may be array (when multiple values allowed) if($.isArray(data)) { //collect selected data and show with separator text = []; @@ -156,7 +143,26 @@ $(function(){ }, value2input: function(value) { - this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + //for remote source .val() is not working, need to look in sourceData + if(this.isRemote) { + //todo: check value for array + var item, items; + //if sourceData loaded, use it to get text for display + if(this.sourceData) { + items = $.fn.editableutils.itemsByValue(value, this.sourceData, 'id'); + if(items.length) { + item = items[0]; + } + } + //if item not found by sourceData, use element text (e.g. for the first show) + if(!item) { + item = {id: value, text: $(this.options.scope).text()}; + } + //select2('data', ...) allows to set both id and text --> usefull for initial show when items are not loaded + this.$input.select2('data', item).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + } else { + this.$input.val(value).trigger('change', true); //second argument needed to separate initial change from user's click (for autosubmit) + } }, input2value: function() { @@ -189,6 +195,22 @@ $(function(){ $(this).closest('form').submit(); } }); + }, + + /* + Converts source from x-editable format: {value: 1, text: "1"} to + select2 format: {id: 1, text: "1"} + */ + convertSource: function(source) { + if($.isArray(source) && source.length && source[0].value !== undefined) { + for(var i = 0; i<source.length; i++) { + if(source[i].value !== undefined) { + source[i].id = source[i].value; + delete source[i].value; + } + } + } + return source; } }); diff --git a/test/index.html b/test/index.html index dca946e..73f4a59 100644 --- a/test/index.html +++ b/test/index.html @@ -21,6 +21,7 @@ <div> <div id="qunit"></div> <div id="qunit-fixture"></div> + <div id="qunit-fixture1"></div> <div id="async-fixture" style="padding-left:300px"></div> </div> </body> diff --git a/test/mocks.js b/test/mocks.js index 1238ed5..2703cdd 100644 --- a/test/mocks.js +++ b/test/mocks.js @@ -36,8 +36,10 @@ $(function () { //groups as array window.groupsArr = []; + window.groupsArr2 = []; for(var i in groups) { groupsArr.push({value: i, text: groups[i]}); + groupsArr2.push({id: i, text: groups[i]}); } window.size = groupsArr.length; @@ -46,6 +48,16 @@ $(function () { url: 'groups.php', responseText: groups }); + + $.mockjax({ + url: 'groupsArr', + responseText: groupsArr + }); + + $.mockjax({ + url: 'groupsArr2', + responseText: groupsArr2 + }); $.mockjax({ url: 'groups-error.php', diff --git a/test/unit/api.js b/test/unit/api.js index aff15c8..e941001 100644 --- a/test/unit/api.js +++ b/test/unit/api.js @@ -66,7 +66,19 @@ $(function () { equal(values.comment, '', 'textarea empty value') ; ok(!('sex' in values), 'select value not present') ; ok(!('dob' in values), 'date value not present') ; - }); + }); + + test("getValue with isSingle = true", function () { + var v = '123', + e = $( + '<a href="#" data-type="text" id="username">'+v+'</a>' + + '<a href="#" data-type="textarea" id="comment">456</a>' + ).appendTo('#qunit-fixture').editable(); + + //check get value + var value = e.editable('getValue', true); + equal(value, v, 'value ok'); + }); test("'init' event", function () { expect(1); @@ -138,12 +150,13 @@ $(function () { }); asyncTest("event: save / hidden (reason: save)", function () { - expect(2); + expect(3); var val = '1', e = $('<a href="#" data-pk="1" data-type="select" data-url="post.php" data-name="text" data-value="'+val+'"></a>').appendTo(fx); e.on('save', function(event, params) { equal(params.newValue, 2, 'save triggered, value correct'); + equal(params.submitValue, '2', 'submitValue property correct'); }); e.on('hidden', function(event, reason) { @@ -167,6 +180,33 @@ $(function () { }, timeout); }); + asyncTest("hide when saving value", function () { + var newVal = 2, + e = $('<a href="#" data-pk="1" data-type="select" data-url="post.php" data-name="text" data-value="1"></a>') + .appendTo(fx) + .editable({ + source: groupsArr + }); + + e.click(); + var p = tip(e); + p.find('select').val(2); + p.find('form').submit(); + + e.parent().click(); + + ok(p.is(':visible'), 'popover still visible'); + + setTimeout(function() { + equal(e.data('editable').value, newVal, 'new value saved'); + ok(!p.is(':visible'), 'popover closed'); + + e.remove(); + start(); + }, timeout); + + }); + test("show/hide/toggle methods", function () { var e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo('#qunit-fixture').editable(); e.editable('show'); diff --git a/test/unit/date.js b/test/unit/date.js index ce3c9c2..cc48248 100644 --- a/test/unit/date.js +++ b/test/unit/date.js @@ -5,7 +5,7 @@ $(function () { module("date", { setup: function(){ fx = $('#async-fixture'); - dpg = $.fn.datepicker.DPGlobal; + dpg = $.fn.bdatepicker.DPGlobal; $.support.transition = false; mode = $.fn.editable.defaults.mode; $.fn.editable.defaults.mode = 'popup'; diff --git a/test/unit/datefield.js b/test/unit/datefield.js index 8433e1d..bb6635c 100644 --- a/test/unit/datefield.js +++ b/test/unit/datefield.js @@ -5,7 +5,7 @@ $(function () { module("datefield", { setup: function(){ fx = $('#async-fixture'); - dpg = $.fn.datepicker.DPGlobal; + dpg = $.fn.bdatepicker.DPGlobal; $.support.transition = false; mode = $.fn.editable.defaults.mode; @@ -105,4 +105,31 @@ $(function () { equal(e.text(), dview, 'text correct'); }); + + test("incorrect date", function () { + var d = '15.05.1984', + e = $('<a href="#" data-type="date" data-pk="1">'+d+'</a>').appendTo('#qunit-fixture').editable({ + format: f, + viewformat: f + }), + nextD = '16.05.1984'; + + equal(frmt(e.data('editable').value, 'dd.mm.yyyy'), d, 'value correct'); + + e.click(); + var p = tip(e); + ok(p.find('input').is(':visible'), 'input exists'); + + equal(p.find('input').val(), d, 'date set correct'); + + //enter incorrect date + p.find('input').val('abcde'); + + //submit + p.find('form').submit(); + + ok(!p.is(':visible'), 'popover closed'); + equal(e.data('editable').value, null, 'date set to null'); + equal(e.text(), $.fn.editable.defaults.emptytext , 'emptytext shown'); + }); }); \ No newline at end of file diff --git a/test/unit/datetimefield.js b/test/unit/datetimefield.js index 9d36eb4..f7c5725 100644 --- a/test/unit/datetimefield.js +++ b/test/unit/datetimefield.js @@ -125,4 +125,24 @@ $(function () { equal(e.text(), dview, 'text correct'); }); + test("incorrect datetime", function () { + var dview = '15/05/1984 15:45', + d = '1984-05-15 15:45', + e = $('<a href="#" data-type="datetime" data-pk="1" data-format="yyyy-mm-dd hh:ii" data-viewformat="dd/mm/yyyy hh:ii" data-value="'+d+'"></a>').appendTo('#qunit-fixture').editable(); + + e.click(); + var p = tip(e); + ok(p.find('input').is(':visible'), 'input exists'); + + //enter incorrect date + p.find('input').val('abcde'); + + //submit + p.find('form').submit(); + + ok(!p.is(':visible'), 'popover closed'); + equal(e.data('editable').value, null, 'date set to null'); + equal(e.text(), $.fn.editable.defaults.emptytext , 'emptytext shown'); + }); + }); \ No newline at end of file diff --git a/test/unit/select.js b/test/unit/select.js index a5b3da8..f6c7561 100644 --- a/test/unit/select.js +++ b/test/unit/select.js @@ -41,13 +41,15 @@ $(function () { //open second time: items should not dublicate e.click(); - - ok(p.find('select').length, 'select exists'); - equal(p.find('select').find('option').length, size, 'options loaded'); - equal(p.find('select').val(), e.data('editable').value, 'selected value correct') ; - - e.remove(); - start(); + p = tip(e); + setTimeout(function() { + ok(p.find('select').length, 'select exists'); + equal(p.find('select').find('option').length, size, 'options loaded'); + equal(p.find('select').val(), e.data('editable').value, 'selected value correct') ; + + e.remove(); + start(); + }, timeout); }, timeout); }); @@ -106,7 +108,8 @@ $(function () { }); test("load options from function returning array", function () { - var e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">customer</a>').appendTo('#qunit-fixture').editable({ + var counter = 0, + e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">customer</a>').appendTo('#qunit-fixture').editable({ pk: 1, prepend: function() { equal(this, e[0], 'prepend scope is element'); @@ -114,22 +117,30 @@ $(function () { }, source: function() { equal(this, e[0], 'source scope is element'); - return groups; + return counter ? groupsArr.concat([{10: 'test'}]) : groupsArr; } }); - e.click() - var p = tip(e); - ok(p.is(':visible'), 'popover visible'); - ok(p.find('select').length, 'select exists'); - equal(p.find('select').find('option').length, size+1, 'options loaded'); - equal(p.find('select').val(), e.data('editable').value, 'selected value correct') ; - p.find('.editable-cancel').click(); - ok(!p.is(':visible'), 'popover was removed'); + function t() { + e.click() + var p = tip(e); + ok(p.is(':visible'), 'popover visible'); + ok(p.find('select').length, 'select exists'); + equal(p.find('select').find('option').length, size+1+counter, 'options loaded'); + equal(p.find('select').val(), e.data('editable').value, 'selected value correct') ; + p.find('.editable-cancel').click(); + ok(!p.is(':visible'), 'popover was removed'); + } + + //first run + t(); + //second time + counter = 1; + t(); }); asyncTest("load options from function returning URL", function () { - var e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">customer</a>').appendTo('#qunit-fixture').editable({ + var e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">customer</a>').appendTo(fx).editable({ pk: 1, //need to disable cache to force request sourceCache: false, @@ -152,13 +163,15 @@ $(function () { //open second time: items should not dublicate e.click(); - - ok(p.find('select').length, 'select exists'); - equal(p.find('select').find('option').length, size, 'options loaded'); - equal(p.find('select').val(), e.data('editable').value, 'selected value correct') ; - - e.remove(); - start(); + p = tip(e); + setTimeout(function() { + ok(p.find('select').length, 'select exists'); + equal(p.find('select option').length, size, 'options loaded'); + equal(p.find('select').val(), e.data('editable').value, 'selected value correct') ; + + e.remove(); + start(); + }, timeout); }, timeout); }); @@ -428,7 +441,7 @@ $(function () { }); - + //since 1.4.5 sourceCache is more strong: it forces load sourceData on every click asyncTest("sourceCache: false", function () { $.mockjax({ @@ -448,6 +461,7 @@ $(function () { }); setTimeout(function() { + //autotext for first only! equal(req, 1, 'autotext request performed'); //click first @@ -459,7 +473,7 @@ $(function () { ok(p.is(':visible'), 'popover visible'); equal(p.find('select').find('option').length, size, 'options loaded'); - equal(req, 1, 'no additional request performed, loaded on autotext'); + equal(req, 2, 'additional request performed (allthough loaded on autotext)'); p.find('.editable-cancel').click(); ok(!p.is(':visible'), 'popover was removed'); @@ -471,7 +485,7 @@ $(function () { setTimeout(function() { ok(p.is(':visible'), 'popover2 visible'); equal(p.find('select').find('option').length, size, 'options loaded'); - equal(req, 2, 'second request performed'); + equal(req, 3, 'second request performed'); p.find('.editable-cancel').click(); ok(!p.is(':visible'), 'popover was removed'); @@ -601,7 +615,7 @@ $(function () { asyncTest("'display' callback", function () { var req = 0, - e = $('<a href="#" data-type="select" data-value="2" data-url="post.php"></a>').appendTo(fx).editable({ + e = $('<a href="#" data-type="select" data-value="2" data-url="post.php">123</a>').appendTo(fx).editable({ pk: 1, source: groups, ajaxOptions: { @@ -690,16 +704,14 @@ $(function () { asyncTest("change source", function () { - var e = $('<a href="#" data-type="select" data-name="load-srv" data-value="2" data-source="groups.php"></a>').appendTo(fx).editable({ - //need to disable cache to force request - sourceCache: false + var e = $('<a href="#" data-type="select" data-name="load-srv" data-value="2" data-source="groups.php">customer</a>').appendTo(fx).editable({ + sourceCache: true }); - + + e.click(); + var p = tip(e); + setTimeout(function() { - - e.click(); - var p = tip(e); - equal(p.find('select').find('option').length, size, 'options loaded'); equal(p.find('select').val(), e.data('editable').value, 'selected value correct') ; @@ -707,12 +719,12 @@ $(function () { ok(!p.is(':visible'), 'popover was closed'); $.mockjax({ - url: 'groups1.php', + url: 'groups-changed.php', responseText: [{value: 'a', text: 1}, {value: 'b', text: 2}] }); //set new source - e.editable('option', 'source', 'groups1.php'); + e.editable('option', 'source', 'groups-changed.php'); e.click(); setTimeout(function() { @@ -728,8 +740,7 @@ $(function () { e.remove(); start(); }, timeout); - - }, timeout); + }, timeout); }); asyncTest("optgroup", function () { diff --git a/test/unit/select2.js b/test/unit/select2.js index 7065fe3..053298a 100644 --- a/test/unit/select2.js +++ b/test/unit/select2.js @@ -8,7 +8,7 @@ $(function () { } }); - asyncTest("data (single)", function () { + asyncTest("init-change-save (not multiple, local)", function () { var s = 2, text = 'text2', e = $('<a href="#" data-type="select2" data-name="select2" data-value="'+s+'"></a>').appendTo(fx).editable({ source: [{id: 1, text: 'text1'}, {id: s, text: text}, {id: 3, text: 'text3'}], @@ -49,7 +49,7 @@ $(function () { }, timeout); }); - asyncTest("data (multiple)", function () { + asyncTest("init-change-save (multiple, local)", function () { var s = '2,3', text = 'text2, text3', e = $('<a href="#" data-type="select2" data-name="select2" data-value="'+s+'"></a>').appendTo(fx).editable({ source: [{id: 1, text: 'text1'}, {id: 2, text: 'text2'}, {id: 3, text: 'text3'}], @@ -97,7 +97,7 @@ $(function () { }, timeout); }); - asyncTest("tags", function () { + asyncTest("tags (local)", function () { var s = 'text2,abc', text = 'text2, abc', e = $('<a href="#" data-type="select2" data-name="select2">'+text+'</a>').appendTo(fx).editable({ viewseparator: ', ', @@ -141,6 +141,84 @@ $(function () { start(); }, timeout); }); - + + test("setValue (local) + x-editable source", function () { + var e = $('<a href="#" data-type="select2" data-name="select2" data-value="1">test2</a>').appendTo('#qunit-fixture').editable({ + source: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, {value: 3, text: 'text3'}] + }); + + //autotext + equal(e.data('editable').value, 1, 'initial value ok'); + + //setValue before open + e.editable('setValue', 2); + equal(e.data('editable').value, 2, 'value ok'); + equal(e.text(), 'text2', 'text ok'); + + //open + e.click(); + var p = tip(e); + p.find('.editable-cancel').click(); + + //setValue after hide + e.editable('setValue', 3); + equal(e.data('editable').value, 3, 'value ok'); + equal(e.text(), 'text3', 'text ok'); + }); + + asyncTest("init-change-save (not multiple, remote)", function () { + var s = 2, text = groups[s], + newVal = 0, newText = groups[newVal], + e = $('<a href="#" data-type="select2" data-name="select2" data-value="'+s+'">'+text+'</a>').appendTo(fx).editable({ + source: 'groupsArr2' + }); + + e.click(); + var p = tip(e); + + ok(p.is(':visible'), 'popover visible'); + equal(p.find('.select2-choice span').text(), text, 'selected text correct'); + + var $input = p.find('input[type="hidden"]'); + ok($input.length, 'input exists'); + ok($input.select2, 'select2 applied'); + equal($input.val(), e.data('editable').value, 'selected value correct'); + + //click to load items + p.find('.select2-choice').mousedown(); + + setTimeout(function() { + equal($('.select2-results li').length, groupsArr2.length, 'items loaded'); + equal($('.select2-results .select2-highlighted > .select2-result-label').text(), text, 'highlight ok'); + + //select new value (0) + $('.select2-results li').eq(newVal).mouseup(); + equal(p.find('.select2-choice span').text(), newText, 'new selected text ok'); + + //submit + p.find('form').submit(); + + setTimeout(function() { + ok(!p.is(':visible'), 'popover closed'); + equal(e.data('editable').value, newVal, 'new value ok'); + equal(e.text(), newText, 'new text ok'); + + //open again + e.click(); + p = tip(e); + equal(p.find('.select2-choice span').text(), newText, 'text ok on second open'); + equal($input.val(), newVal, 'selected value ok on second open'); + + //setValue in closed state + p.find('.editable-cancel').click(); + e.editable('setValue', 1); + equal(e.data('editable').value, 1, 'setValue: value ok'); + equal(e.text(), groups[1], 'setValue: text ok'); + + e.remove(); + start(); + }, timeout); + }, timeout); + }); }); \ No newline at end of file diff --git a/test/unit/text.js b/test/unit/text.js index 66da9a0..523f015 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -374,17 +374,27 @@ $(function () { asyncTest("'display' callback", function () { var newText = 'cd<e>;"', - e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">abc</a>').appendTo(fx).editable({ + counter = 0, + initialVal = 'abc', + e = $('<a href="#" data-pk="1" data-url="post.php" data-name="text1">123</a>').appendTo(fx).editable({ ajaxOptions: { dataType: 'json' }, display: function(value, response) { - ok(this === e[0], 'scope is ok'); - ok(response.success, 'response param ok'); - $(this).text('qq'+value); - } - }); + if(counter === 0) { + ok(response === undefined, 'initial autotext ok as display is func'); + $(this).text(initialVal); + } else { + ok(this === e[0], 'updating, scope is ok'); + ok(response.success, 'response param ok'); + $(this).text('qq'+value); + } + counter++; + } + }); + equal(e.text(), initialVal, 'initial autotext ok'); + e.click() var p = tip(e); @@ -424,7 +434,28 @@ $(function () { start(); }, timeout); - }); + }); + + test("'display' returning html only (img)", function () { + var c = 0, + html = '<img src="../src/img/clear.png">', + html_br = '<br>', + e = $('<a href="#" data-pk="1" data-type="text" data-name="text1">0</a>').appendTo('#qunit-fixture').editable({ + display: function(value, response) { + $(this).html(c == 0 ? html : html_br); + } + }); + + equal(e.html(), html, 'html ok'); + + c = 1; + e.click() + var p = tip(e); + p.find('input').val(1); + p.find('form').submit(); + + equal(e.html(), $.fn.editable.defaults.emptytext, 'html br --> emptytext ok'); + }); test("password", function () { var v = '123', v1 = '456'; diff --git a/test/unit/wysihtml5.js b/test/unit/wysihtml5.js index 58ba23f..9b01d66 100644 --- a/test/unit/wysihtml5.js +++ b/test/unit/wysihtml5.js @@ -6,6 +6,8 @@ $(function () { $.support.transition = false; } }); + + //note: sometimes it throws 'nativeSelection is null' error asyncTest("should load correct value and save new entered value", function () {