├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── LICENSE.txt ├── README.md ├── bower.json ├── build ├── jqm.editable.listview.css ├── jqm.editable.listview.js ├── jqm.editable.listview.min.css └── jqm.editable.listview.min.js ├── css └── editable-listview.css ├── demo ├── demo.css └── index.html ├── editable-listview.png ├── gulpfile.js ├── js └── editable-listview.js ├── package.json └── tests ├── functional ├── casper.spec.js └── simple.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/**/* 2 | node_modules/**/* 3 | wiki/**/* -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "jquery" 3 | } 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 100, // {int} maximum amount of warnings JSHint will produce before giving up. Default: 50. 3 | "regexp" : true, // ? 4 | 5 | // Enforcing 6 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 7 | "curly" : true, // true: Require {} for every new block or scope 8 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 9 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 10 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 11 | "latedef" : true, // true: Require variables/functions to be defined before being used 12 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 13 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 14 | "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) 15 | "plusplus" : false, // true: Prohibit use of `++` & `--` 16 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 17 | "unused" : true, // true Require all defined variables be used 18 | // "vars" to only check for variables, not function parameters, 19 | // "strict" to check all variables and parameters. 20 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 21 | "maxparams" : false, // {int} Max number of formal params allowed per function 22 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 23 | "maxstatements" : false, // {int} Max number statements per function 24 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 25 | 26 | // Relaxing 27 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 28 | "boss" : true, // true: Tolerate assignments where comparisons would be expected 29 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 30 | "eqnull" : true, // true: Tolerate use of `== null` 31 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 32 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 33 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 34 | // (ex: `for each`, multiple try/catch, function expression…) 35 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 36 | "expr" : true, // true: Tolerate `ExpressionStatement` as Programs 37 | "funcscope" : false, // true: Tolerate defining variables inside control statements 38 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 39 | "iterator" : false, // true: Tolerate using the `__iterator__` property 40 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 41 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 42 | "laxcomma" : false, // true: Tolerate comma-first style coding 43 | "loopfunc" : false, // true: Tolerate functions being defined in loops 44 | "multistr" : false, // true: Tolerate multi-line strings 45 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 46 | "notypeof" : false, // true: Tolerate invalid typeof operator values 47 | "proto" : false, // true: Tolerate using the `__proto__` property 48 | "scripturl" : false, // true: Tolerate script-targeted URLs 49 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 50 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 51 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 52 | "validthis" : false, // true: Tolerate using this in a non-constructor function 53 | 54 | // Environments 55 | "browser" : true, // Web Browser (window, document, etc) 56 | "browserify" : false, // Browserify (node.js code in the browser) 57 | "couch" : false, // CouchDB 58 | "devel" : true, // Development/debugging (alert, confirm, etc) 59 | "dojo" : false, // Dojo Toolkit 60 | "jasmine" : false, // Jasmine 61 | "jquery" : true, // jQuery 62 | "mocha" : true, // Mocha 63 | "mootools" : false, // MooTools 64 | "node" : true, // Node.js 65 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 66 | "prototypejs" : false, // Prototype and Scriptaculous 67 | "qunit" : false, // QUnit 68 | "rhino" : false, // Rhino 69 | "shelljs" : false, // ShellJS 70 | "worker" : false, // Web Workers 71 | "wsh" : false, // Windows Scripting Host 72 | "yui" : false, // Yahoo User Interface 73 | 74 | "onecase" : false, // ? 75 | "regexdash" : false, // ? 76 | 77 | // Custom Globals 78 | // additional predefined global variables 79 | "globals" : { 80 | "require": false, 81 | "define": false 82 | }, 83 | 84 | "quotmark": "double", 85 | "immed": true 86 | } 87 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "iojs" 5 | - "iojs-v1.0.4" 6 | before_install: 7 | - git clone git://github.com/n1k0/casperjs.git ~/casperjs 8 | - cd ~/casperjs 9 | - git checkout tags/1.0.2 10 | - export PATH=$PATH:`pwd`/bin 11 | - cd - 12 | before_script: 13 | - phantomjs --version 14 | - casperjs --version 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Wasif Hasan Baig 4 | 5 | https://github.com/baig/jquerymobile-editablelistview 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Editable Listview (jQuery Mobile Plugin) 2 | ======================================== 3 | A customized version of the [jQuery Mobile Listview Widget](http://demos.jquerymobile.com/1.4.2/listview/) that supports insertion of new list items and removal of existing list items out of the box. 4 | 5 | ![Editable Listview Plugin](editable-listview.png?raw=true) 6 | 7 | ## Why 8 | Many a times, you come across a situation where you want to allow editing of list items. To do this simple task, you have to build extra functionality around the Listview widget to allow for insertion of new list items and removal of existing list items. **Editable Listview** plugin is designed to take the pain out of this situaiton by having all this functionality baked-in. 9 | 10 | ## Features 11 | 12 | 1. Allows insertion of new list items right in the Listview. 13 | 2. Allows easy removal of existing list items. 14 | 15 | ## Installation 16 | 1. Pick any one of the following way. 17 | 18 | * Download and unzip the package into your project folder (Scroll up and you will see the "Download ZIP" button on the right side). 19 | 20 | * Install using bower. 21 | 22 | `bower install jqm-editable-istview` 23 | 24 | * Use the CDN hosted version. 25 | 26 | __js:__ `//cdn.jsdelivr.net/jquery.editable-listview/x.y.z/jqm.editable.listview.min.js` 27 | __css:__ `//cdn.jsdelivr.net/jquery.editable-listview/x.y.z/jqm.editable.listview.min.css` 28 | 29 | where `x.y.z` is the version number. 30 | 31 | 2. Include the javascript file after the jQuery Mobile javascript file. Similarly include the stylesheet after the jQuery Mobile stylesheet 32 | 33 | ## How to Use it? 34 | The listview comes in two flavors: __simple__ and __complex__. Their usage is described below. 35 | 36 | ### Simple Type 37 | Use the following HTML/DOM structure 38 | 39 | ```H 40 | 46 | ``` 47 | 48 | See [below](#attributes) for a full list of available "`data-`" attributes 49 | 50 | ### Complex Type 51 | For complex type, link a form to the listview through `data-editable-form` attribute by putting in the id of the form as the value. That form will be shown embedded in the collapsible listview in `Edit` Mode. The user is at liberty to make the form look however they feel like, but they are supposed to add some specific `data` attributes to form elements. See the working example below. 52 | 53 | ```H 54 | 77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 |
85 | 86 | ``` 87 | 88 | `data-item-name` is the name of the variable to hold the value of the input field. 89 | 90 | `data-item-template` holds the HTML template that will be used to render the new value. Use `%%` as placeholder for the variable. `data-add-button` indicates the button that can be clicked/tapped/pressed to insert the new list item having values specified in the input fields. `data-clear-button` clears all the text from the input fields. 91 | 92 | ## Roadmap 93 | This is a preliminary list of planned fatures. 94 | 95 | 1. In-place editing of existing list items. 96 | 2. Re-ordering of list items tap and hold on the list item and drag it back and forth. 97 | 3. Deleting item by swiping left or right. 98 | 99 | ## List of `data-` Attributes 100 | See the [List of data attributes](https://github.com/baig/jquerymobile-editablelistview/wiki/List-of-data-attributes) wiki page. 101 | 102 | ## License 103 | Copyright © 2014 – 2015 [Wasif Hasan Baig](https://twitter.com/_wbaig) 104 | 105 | Source code is released under the Terms and Conditions of [MIT License](http://opensource.org/licenses/MIT). 106 | 107 | Please refer to the [License file](https://github.com/baig/jquerymobile-editablelistview/blob/master/LICENSE.txt) in the source code project directory. 108 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jqm-editable-listview", 3 | "version": "0.3.1", 4 | 5 | "main": [ 6 | "build/jquery.mobile.editablelistview.js", 7 | "build/jquery.mobile.editablelistview.min.js", 8 | "build/jquery.mobile.editablelistview.css", 9 | "build/jquery.mobile.editablelistview.min.css" 10 | ], 11 | 12 | "dependencies": { 13 | "jqm-extended-collapsible": "~0.0.1" 14 | }, 15 | 16 | "ignore": [ 17 | "demo", 18 | "css", 19 | "js", 20 | "node_modules", 21 | ".gitignore", 22 | "README.md", 23 | "package.json", 24 | "gulpfile.js", 25 | "tests" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /build/jqm.editable.listview.css: -------------------------------------------------------------------------------- 1 | /* Collapsible CSS Patch */ 2 | .ui-collapsible-heading.ui-header, 3 | .ui-collapsible-heading.ui-header > .ui-btn { 4 | cursor: pointer; 5 | -webkit-border-radius: .3125em; 6 | border-radius: .3125em; 7 | } 8 | /* --- End of Patch ---- */ 9 | .ui-collapsible-heading.ui-header > h1,h2,h3,h4,h5,h6 { 10 | text-align: left; 11 | margin-left: 40px; 12 | } 13 | .ui-editable-flex { 14 | width: 100%; 15 | display: inline-flex; 16 | flex-direction: row; 17 | } 18 | .ui-editable-border-right { 19 | border-top-right-radius: .3125em; 20 | border-bottom-right-radius: .3125em; 21 | } 22 | .ui-editable-border-left { 23 | border-top-left-radius: .3125em; 24 | border-bottom-left-radius: .3125em; 25 | } 26 | .ui-editable-flex-item-right { 27 | flex: 1 1 auto; 28 | } 29 | /* Large desktop */ 30 | @media (min-width: 1200px) { 31 | .ui-editable-flex-item-left { 32 | flex: 35 1 auto; 33 | } 34 | } 35 | 36 | /* Landscape tablet and dated desktop */ 37 | @media (min-width: 980px) and (max-width: 1199px) { 38 | .ui-editable-flex-item-left { 39 | flex: 25 1 auto; 40 | } 41 | } 42 | 43 | /* Portrait tablet to landscape and desktop */ 44 | @media (min-width: 767px) and (max-width: 979px) { 45 | .ui-editable-flex-item-left { 46 | flex: 15 1 auto; 47 | } 48 | } 49 | 50 | /* Landscape phone to portrait tablet */ 51 | @media (max-width: 767px) { 52 | .ui-editable-flex-item-left { 53 | flex: 15 1 auto; 54 | } 55 | } 56 | 57 | /* Landscape phones and down */ 58 | @media (max-width: 480px) { 59 | .ui-editable-flex-item-left { 60 | flex: 5 1 auto; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /build/jqm.editable.listview.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Mobile Editable Listview Plugin 3 | * https://github.com/baig/jquerymobile-editablelistview 4 | * 5 | * Copyright 2014 (c) Wasif Hasan Baig 6 | * 7 | * Released under the MIT license 8 | * https://github.com/baig/jquerymobile-editablelistview/blob/master/LICENSE.txt 9 | */ 10 | 11 | (function ($, undefined) { 12 | 13 | "use strict"; 14 | 15 | $.widget("mobile.listview", $.mobile.listview, { 16 | 17 | // Declaring some private instace state variables 18 | _created: false, 19 | _origDom: null, 20 | _editMode: false, 21 | _counter: 1, 22 | _dataItemName: "item", 23 | _evt: null, 24 | _clickHandler: null, 25 | _tapHandler: null, 26 | 27 | // The options hash 28 | options: { 29 | editable: false, 30 | editableType: 'simple', 31 | editableForm: '', 32 | itemName: '', 33 | 34 | title: "View list items", 35 | emptyTitle: "No items to view", 36 | editLabel: "Edit", 37 | addLabel: "Add", 38 | doneLabel: "Done", 39 | addIcon: "plus", 40 | editIcon: "edit", 41 | doneIcon: "check", 42 | 43 | buttonTheme: 'a', 44 | buttonCorner: true, 45 | buttonShadow: true, 46 | 47 | itemIcon: false, 48 | 49 | collapsed: false, 50 | expandedIcon: 'carat-d', 51 | collapsedIcon: 'carat-r' 52 | }, 53 | 54 | _beforeListviewRefresh: function () { 55 | 56 | // Returning immediately if `data-editable="false"` 57 | if (!this.options.editable) return; 58 | 59 | var $el = this.element, 60 | opts = this.options, 61 | $origDom = this._origDom, 62 | dataItemName = this._dataItemName, 63 | counter = this._counter, 64 | ui = {}, 65 | $orig, 66 | $origLis, 67 | self = this, 68 | evt = this._evt, 69 | $lis = $el.find("li"), 70 | $markup = this._$markup; 71 | 72 | /** 73 | * saving original DOM structure if there is a discrepency in the number 74 | * of list items between `this.element` and `this._origDom` or if the 75 | * or if `this._origDom` is null 76 | * Note: list item length count ignores the list item housing the 77 | * text box 78 | * Fix for Bug #4 79 | */ 80 | // ## Creation 81 | if (!this._created) { 82 | if ($el.find('li').not('li.ui-editable-temp').length !== ($origDom === null ? -1 : $origDom.find('li').length)) { 83 | $origDom = $el.clone(); 84 | // Assign each list item a unique number value 85 | $.each($origDom.children('li'), function (idx, val) { 86 | $(val).attr("data-" + dataItemName, counter); 87 | counter++; 88 | }); 89 | // Incrementing the counter that is used to assign unique value to `data-item` attribute on each list item 90 | this._counter = counter; 91 | // Caching the original list to the widget instance 92 | this._origDom = this._getUnenhancedList($origDom); 93 | } 94 | 95 | // Wrapping the list structure inside Collapsible 96 | ui.wrapper = this._wrapCollapsible(); 97 | ui.header = ui.wrapper.find('.ui-collapsible-heading') 98 | ui.button = ui.header.find('a a, a button') 99 | ui.content = ui.wrapper.find('.ui-collapsible-content') 100 | ui.form = this._getForm(); 101 | 102 | $.extend(this, { 103 | _ui: ui, 104 | _newItems: [], 105 | _items: {} 106 | }) 107 | 108 | this._items = this._getExistingListContents(); 109 | 110 | evt = this._evt = $._data(ui.header[0], "events"); 111 | this._clickHandler = evt.click[0].handler; 112 | this._tapHandler = evt.tap[0].handler 113 | 114 | this._attachEditEventButtons(); 115 | 116 | this._created = true; 117 | } 118 | 119 | if (this._editMode) { 120 | 121 | ui = this._ui; 122 | $orig = $origDom.clone(); 123 | $origLis = $orig.find('li'); 124 | 125 | if ($orig.find('li.ui-editable-temp').length === 0) { 126 | // Checking if text content of
  • is wrapped inside 127 | if ($orig.find('a').length === 0) { 128 | // wrapping contents of
  • inside 129 | $origLis.wrapInner('') 130 | } 131 | 132 | // appending another inside
  • ; this is the delete button 133 | $origLis.append('Delete'); 134 | 135 | this.option("splitIcon", "minus"); 136 | 137 | if (opts.editableType === 'complex') { 138 | $orig.prepend('
  • '); 139 | $orig.find("li.ui-editable-temp").append(ui.form); 140 | } 141 | if (opts.editableType === 'simple') { 142 | $orig.prepend($markup.listTextInput); 143 | } 144 | } 145 | 146 | $lis.remove(); 147 | 148 | $el.append($orig.find('li')); 149 | 150 | /** 151 | * Disabling the click and tap event handlers on header when the 152 | * list is in `Edit` mode 153 | */ 154 | evt.click[0].handler = evt.tap[0].handler = function (e) { 155 | e.stopPropagation(); 156 | e.preventDefault(); 157 | } 158 | } else { 159 | 160 | // Re-enabling the click event handler when the list is in `View` mode 161 | evt.click[0].handler = this._clickHandler; 162 | evt.tap[0].handler = this._tapHandler; 163 | 164 | // Removing `Edit` mode `Li`s 165 | $lis.filter(".ui-editable-temp").hide() 166 | $lis.not(".ui-editable-temp").remove() 167 | 168 | if (opts.itemIcon) { 169 | $el.append($origDom.clone().find('li')); 170 | } else { 171 | $el.append($origDom.clone().find('li').attr("data-icon", "false")); 172 | } 173 | } 174 | 175 | // Updating the header title, header button label and icon based on the list contents and its state (`Edit` or `View`) 176 | this._updateHeader(); 177 | }, 178 | 179 | _getExistingListContents: function() { 180 | var opts = this.options, 181 | ui = this._ui, 182 | origDom = this._origDom, 183 | items = {}, 184 | tmpObj = {}; 185 | 186 | if (this.options.editableType === 'simple') { 187 | this._origDom.find('li').each( function(idx, li) { 188 | var $li = $(li) 189 | items[$li.data('item')] = $li.text() 190 | }) 191 | } 192 | 193 | if (opts.editableType === 'complex') { 194 | 195 | ui.form.find('input').each( function( idx, input ) { 196 | var dataItemName = $(input).data('item-name') 197 | tmpObj[dataItemName] = dataItemName 198 | }) 199 | 200 | this._origDom.find('li').each( function(idx, li) { 201 | var $li = $(li) 202 | var htmlStr = $li.html() 203 | 204 | var obj = {} 205 | 206 | $.each(tmpObj, function(dataItemName) { 207 | var $span = $li.find('span#' + dataItemName) 208 | var itemValue = $span.data('value') 209 | if (itemValue !== undefined) { 210 | obj[dataItemName] = itemValue 211 | } else { 212 | obj[dataItemName] = $span.text() 213 | } 214 | }) 215 | 216 | items[$li.data('item')] = obj 217 | }) 218 | } 219 | 220 | return items; 221 | }, 222 | 223 | _getForm: function() { 224 | var $el = this.element, 225 | opts = this.options, 226 | ui = this._ui, 227 | form = null; 228 | 229 | if (opts.editableType === 'complex') { 230 | if (ui && ui.form) { 231 | return ui.form 232 | } else { 233 | try { 234 | if (opts.editableForm.length === 0) { 235 | throw new Error("Form not specified for the Complex Editable Listview type.") 236 | } 237 | form = $el.closest(':jqmData(role="page")') 238 | .find('#' + opts.editableForm); 239 | if (!form.length) { 240 | throw new Error("No form found. Specify a form.") 241 | } 242 | if (!form.is("form, div") && !form.attr("data-editable-form")) { 243 | throw new Error("In case of Complex Editable type, the form's id should match the \"data-editable-form\" attribute on ul tag, and the form element itself should have data-editable-form=\"true\" attribute.") 244 | } 245 | form = form.detach(); 246 | } catch (error) { 247 | console.error(error.message) 248 | } 249 | } 250 | } 251 | 252 | return form; 253 | }, 254 | 255 | _getUnenhancedList: function($dom) { 256 | // removing all CSS classes to get the original list structure 257 | return $dom.removeClass("ui-listview ui-shadow ui-corner-all ui-listview-inset ui-group-theme-" + this.options.theme) 258 | .find("li") 259 | .removeClass("ui-li-static ui-body-inherit ui-first-child ui-last-child ui-li-has-alt") 260 | .end() 261 | .find("a") 262 | .removeClass("ui-link") 263 | .end(); 264 | }, 265 | 266 | _afterListviewRefresh: function () { 267 | if (this.options.editable) { 268 | this._attachDetachEventHandlers(); 269 | } 270 | }, 271 | 272 | _wrapCollapsible: function () { 273 | var $el = this.element; 274 | 275 | $el.wrap("
    ") 276 | .before(this._$markup.header(this.options, this._isListEmpty())) 277 | 278 | return $el.closest(":jqmData(role='collapsible')").collapsible(); 279 | }, 280 | 281 | _attachEditEventButtons: function () { 282 | if (this._isListEmpty()) { 283 | this._ui.header.off("click"); 284 | } 285 | 286 | this._on(this._ui.button, { 287 | "click": "_onEditButtonTapped" 288 | }); 289 | }, 290 | 291 | // --(start)-- Event Handlers -- 292 | 293 | _onEditButtonTapped: function (e) { 294 | e.preventDefault(); 295 | e.stopPropagation(); 296 | 297 | var editMode = this._editMode = !this._editMode, 298 | $collapsible = this.element.parents(":jqmData(role='collapsible')"); 299 | 300 | editMode ? $collapsible.collapsible("expand") : $collapsible.collapsible("collapse"); 301 | 302 | this.refresh(); 303 | 304 | if (!editMode) { 305 | this._triggerListChange(e); 306 | } 307 | }, 308 | 309 | _updateHeader: function () { 310 | var ui = this._ui, 311 | opts = this.options, 312 | isListEmpty = this._isListEmpty(); 313 | 314 | // updating list header title 315 | ui.header 316 | .children("a")[0] 317 | .childNodes[0] 318 | .data = (isListEmpty) ? opts.emptyTitle : opts.title; 319 | 320 | // changing "Edit" button state, icon and label 321 | ui.button.removeClass('ui-icon-minus ui-icon-' + opts.doneIcon + ' ui-icon-' + opts.addIcon + ' ui-icon-' + opts.editIcon) 322 | .addClass('ui-icon-' + (this._editMode ? opts.doneIcon : isListEmpty ? opts.addIcon : opts.editIcon)) 323 | .text(this._editMode ? opts.doneLabel : isListEmpty ? opts.addLabel : opts.editLabel); 324 | 325 | }, 326 | 327 | // _triggerListChange 328 | _triggerListChange: function (e) { 329 | var opts = this.options 330 | this._trigger('change', e, { 331 | items: (opts.editableType === 'simple' && opts.itemName !== '') ? this._toObjectCollection(this._toArray(this._items), opts.itemName) : this._toArray(this._items), 332 | added: (opts.editableType === 'simple' && opts.itemName !== '') ? this._toObjectCollection(this._newItems, opts.itemName) : this._newItems, 333 | length: this.length(), 334 | }); 335 | // emptying the _newItems array 336 | this._newItems = [] 337 | }, 338 | 339 | // --(end)-- Event Handlers -- 340 | 341 | // --(start)-- Event Handler Helper Functions -- 342 | 343 | _attachDetachEventHandlers: function () { 344 | this._enableInsertListItemEvent(); 345 | this._enableListItemDeleteEvent(); 346 | 347 | // this._enableListItemEditing() // v0.3 348 | }, 349 | 350 | 351 | _enableInsertListItemEvent: function () { 352 | var $addBtn, $clearBtn, $textField, 353 | opts = this.options, 354 | editableType = opts.editableType, 355 | $content = this._ui.content; 356 | 357 | if (this._editMode) { 358 | $addBtn = (editableType === 'simple') ? $content.find('li.ui-editable-temp a#item-add') : $content.find("li:first-child [data-add-button='true']"), 359 | $clearBtn = (editableType === 'complex') ? $content.find("li:first-child [data-clear-button='true']") : null, 360 | $textField = (editableType === 'simple') ? $content.find('input[type=text]') : null; 361 | 362 | this._off( $addBtn, "tap" ); 363 | this._on($addBtn, { 364 | "tap": "_insertListItem" 365 | }); 366 | 367 | if ($clearBtn !== null) { 368 | this._off( $clearBtn, "tap" ); 369 | this._on($clearBtn, { 370 | "tap": "_clearTextFields" 371 | }); 372 | } 373 | 374 | if ($textField !== null) { 375 | this._off( $textField, "keyup" ); 376 | this._on($textField, { 377 | "keyup": "_insertListItem" 378 | }); 379 | } 380 | } 381 | }, 382 | 383 | _clearTextFields: function (e) { 384 | e.preventDefault(); 385 | 386 | var inputs = $(e.target).parents('li').find('[data-item-name]'); 387 | 388 | $.each(inputs, function (idx, val) { 389 | $(val).val(""); 390 | }); 391 | }, 392 | 393 | _enableListItemDeleteEvent: function () { 394 | this._editMode 395 | ? this._on(this._ui.content.find('a.ui-editable-btn-del'), { "click": "_deleteListItem" }) 396 | : this._off(this._ui.content.find('a.ui-editable-btn-del'), "click"); 397 | }, 398 | 399 | 400 | // TODO v0.3 401 | /*_enableListItemEditing: function() {},*/ 402 | 403 | _insertListItem: function (e) { 404 | e.preventDefault(); 405 | 406 | var $el = this.element, 407 | itemObj = {}; 408 | 409 | // returning immediately if keyup keycode does not match keyCode.ENTER i.e. 13 410 | if (e.type !== "tap" && e.keyCode !== $.mobile.keyCode.ENTER) return; 411 | 412 | if (this.options.editableType === 'complex') { 413 | 414 | var liTemplate = '', 415 | proceed = true, 416 | $inputs = $(e.target).parents('li').find('[data-item-name]'); 417 | 418 | $.each($inputs, function (idx, val) { 419 | var $input = $(val), 420 | template = $input.data("item-template"), 421 | inputType = $input.attr("type"), 422 | itemName = $input.data("item-name"), 423 | value = null; 424 | 425 | switch(inputType) { 426 | case "text": 427 | case "number": 428 | value = $input.val() 429 | itemObj[itemName] = value 430 | $input.val("") 431 | break 432 | case "checkbox": 433 | value = $input.is(":checked") 434 | itemObj[itemName] = value 435 | break 436 | case "radio": 437 | var itemName = $input.attr("name") 438 | var $radios = $el.find("li:first-child input[data-item-name='" + itemName + "']").filter(":radio") 439 | $radios.each(function() { 440 | var $this = $(this) 441 | if ( $this.is( ":checked" ) ) { 442 | value = $this.data("item-display-value") 443 | itemObj[itemName] = $this.val() 444 | } 445 | }) 446 | break 447 | } 448 | 449 | if (!value && inputType !== "checkbox") { 450 | proceed = false; 451 | } 452 | 453 | var renderedTemplate = template.replace(/%%/, value) 454 | 455 | liTemplate += ( liTemplate.indexOf(renderedTemplate) === -1 ) 456 | ? renderedTemplate // Add only if not already present 457 | : '' // Skip if value already present in liTemplate 458 | }); 459 | 460 | // Not proceeding to add if any input value is empty 461 | if (!proceed) return; 462 | 463 | var dataItemNumber = this._counter 464 | this._counter++; 465 | 466 | this._newItems.push(itemObj) 467 | this._items[dataItemNumber] = itemObj 468 | 469 | liTemplate = $("
  • " + liTemplate + "
  • "); 470 | liTemplate.attr("data-" + this._dataItemName, dataItemNumber); 471 | 472 | this._origDom.prepend(liTemplate); 473 | 474 | this.refresh(); 475 | 476 | } 477 | 478 | if (this.options.editableType === 'simple') { 479 | 480 | var $target = $(e.target), 481 | $input = (e.type === "keyup") ? $target : $target.prev().find('input'), 482 | inputTextString = $input.val(); 483 | 484 | // Inserting list item only if input string is not empty 485 | if (!!inputTextString) { 486 | $input.val(""); // Clearing the input text field 487 | 488 | var dataItemNumber = this._counter 489 | this._counter++; 490 | 491 | this._newItems.push(inputTextString) 492 | this._items[dataItemNumber] = inputTextString 493 | 494 | var liTemplate = this._isListEmpty() 495 | ? $('
  • ') // simple static list template is list is empty 496 | : this._origDom.find('li').first().clone(); // 497 | 498 | liTemplate.attr("data-" + this._dataItemName, dataItemNumber); 499 | 500 | if (liTemplate.children().length === 0) { 501 | liTemplate.text(inputTextString); 502 | } else { 503 | liTemplate.children('a').text(inputTextString); 504 | } 505 | 506 | this._origDom.prepend(liTemplate) 507 | 508 | this.refresh() 509 | } 510 | } 511 | }, 512 | 513 | _deleteListItem: function (e) { 514 | e.preventDefault(); 515 | e.stopPropagation(); 516 | 517 | var $parentTarget = $(e.currentTarget).parent(), 518 | itemNum = $parentTarget.data(this._dataItemName); 519 | 520 | this._origDom.find("li[data-" + this._dataItemName + "=\"" + itemNum + "\"]") 521 | .remove(); 522 | 523 | delete this._items[itemNum] 524 | 525 | $parentTarget.remove(); 526 | 527 | this._updateHeader(); 528 | }, 529 | 530 | // --(end)-- Event Handler Helper Functions -- 531 | 532 | /* 533 | _destroy: function() { 534 | var ui = this._ui, 535 | opts = this.options, 536 | $ul = ui.content.filter('ul'), 537 | $li = $ul.find('li'), 538 | items = this.items() 539 | 540 | // Not doing anything if DOM was already enhanced 541 | if ( opts.enhanced ) { 542 | return this; 543 | } 544 | 545 | ui.header.remove() 546 | ui.content = ui.content.unwrap().unwrap() 547 | 548 | $ul.removeClass("ui-listview ui-corner-all ui-shadow ui-collapsible-collapsed") 549 | $ul.find('a').remove() 550 | this._removeFirstLastClasses($li) 551 | $li.removeClass('ui-li-has-alt') 552 | $li.each( function(idx, val) { 553 | this.textContent = items[idx] 554 | }) 555 | 556 | return ui 557 | },*/ 558 | 559 | _isListEmpty: function () { 560 | return (this.element.find('li').not('li.ui-editable-temp').length === 0) ? true : false; 561 | }, 562 | 563 | _$markup: { 564 | 565 | header: function(opts, isListEmpty) { 566 | return "

    "+ opts.title + 567 | "" + 574 | "

    "; 575 | }, 576 | 577 | listTextInput: "
  • " + 578 | "
    " + 579 | "
    " + 580 | "" + 581 | "
    " + 582 | "Add" + 583 | "
    " + 584 | "
  • ", 585 | 586 | }, 587 | 588 | _toArray: function(obj) { 589 | var arr = [] 590 | var keys = Object.keys(obj) 591 | 592 | for (var i=0; i.ui-btn{cursor:pointer;-webkit-border-radius:.3125em;border-radius:.3125em}.ui-collapsible-heading.ui-header>h1,h2,h3,h4,h5,h6{text-align:left;margin-left:40px}.ui-editable-flex{width:100%;display:inline-flex;flex-direction:row}.ui-editable-border-right{border-top-right-radius:.3125em;border-bottom-right-radius:.3125em}.ui-editable-border-left{border-top-left-radius:.3125em;border-bottom-left-radius:.3125em}.ui-editable-flex-item-right{flex:1 1 auto}@media (min-width:1200px){.ui-editable-flex-item-left{flex:35 1 auto}}@media (min-width:980px) and (max-width:1199px){.ui-editable-flex-item-left{flex:25 1 auto}}@media (min-width:767px) and (max-width:979px){.ui-editable-flex-item-left{flex:15 1 auto}}@media (max-width:767px){.ui-editable-flex-item-left{flex:15 1 auto}}@media (max-width:480px){.ui-editable-flex-item-left{flex:5 1 auto}} -------------------------------------------------------------------------------- /build/jqm.editable.listview.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Mobile Editable Listview Plugin 3 | * https://github.com/baig/jquerymobile-editablelistview 4 | * 5 | * Copyright 2014 (c) Wasif Hasan Baig 6 | * 7 | * Released under the MIT license 8 | * https://github.com/baig/jquerymobile-editablelistview/blob/master/LICENSE.txt 9 | */ 10 | !function(t,e){"use strict";t.widget("mobile.listview",t.mobile.listview,{_created:!1,_origDom:null,_editMode:!1,_counter:1,_dataItemName:"item",_evt:null,_clickHandler:null,_tapHandler:null,options:{editable:!1,editableType:"simple",editableForm:"",itemName:"",title:"View list items",emptyTitle:"No items to view",editLabel:"Edit",addLabel:"Add",doneLabel:"Done",addIcon:"plus",editIcon:"edit",doneIcon:"check",buttonTheme:"a",buttonCorner:!0,buttonShadow:!0,itemIcon:!1,collapsed:!1,expandedIcon:"carat-d",collapsedIcon:"carat-r"},_beforeListviewRefresh:function(){if(this.options.editable){var e,i,n=this.element,a=this.options,l=this._origDom,s=this._dataItemName,o=this._counter,d={},r=this._evt,h=n.find("li"),u=this._$markup;this._created||(n.find("li").not("li.ui-editable-temp").length!==(null===l?-1:l.find("li").length)&&(l=n.clone(),t.each(l.children("li"),function(e,i){t(i).attr("data-"+s,o),o++}),this._counter=o,this._origDom=this._getUnenhancedList(l)),d.wrapper=this._wrapCollapsible(),d.header=d.wrapper.find(".ui-collapsible-heading"),d.button=d.header.find("a a, a button"),d.content=d.wrapper.find(".ui-collapsible-content"),d.form=this._getForm(),t.extend(this,{_ui:d,_newItems:[],_items:{}}),this._items=this._getExistingListContents(),r=this._evt=t._data(d.header[0],"events"),this._clickHandler=r.click[0].handler,this._tapHandler=r.tap[0].handler,this._attachEditEventButtons(),this._created=!0),this._editMode?(d=this._ui,e=l.clone(),i=e.find("li"),0===e.find("li.ui-editable-temp").length&&(0===e.find("a").length&&i.wrapInner(""),i.append('Delete'),this.option("splitIcon","minus"),"complex"===a.editableType&&(e.prepend('
  • '),e.find("li.ui-editable-temp").append(d.form)),"simple"===a.editableType&&e.prepend(u.listTextInput)),h.remove(),n.append(e.find("li")),r.click[0].handler=r.tap[0].handler=function(t){t.stopPropagation(),t.preventDefault()}):(r.click[0].handler=this._clickHandler,r.tap[0].handler=this._tapHandler,h.filter(".ui-editable-temp").hide(),h.not(".ui-editable-temp").remove(),n.append(a.itemIcon?l.clone().find("li"):l.clone().find("li").attr("data-icon","false"))),this._updateHeader()}},_getExistingListContents:function(){var i=this.options,n=this._ui,a=(this._origDom,{}),l={};return"simple"===this.options.editableType&&this._origDom.find("li").each(function(e,i){var n=t(i);a[n.data("item")]=n.text()}),"complex"===i.editableType&&(n.form.find("input").each(function(e,i){var n=t(i).data("item-name");l[n]=n}),this._origDom.find("li").each(function(i,n){var s=t(n),o=(s.html(),{});t.each(l,function(t){var i=s.find("span#"+t),n=i.data("value");o[t]=n!==e?n:i.text()}),a[s.data("item")]=o})),a},_getForm:function(){var t=this.element,e=this.options,i=this._ui,n=null;if("complex"===e.editableType){if(i&&i.form)return i.form;try{if(0===e.editableForm.length)throw new Error("Form not specified for the Complex Editable Listview type.");if(n=t.closest(':jqmData(role="page")').find("#"+e.editableForm),!n.length)throw new Error("No form found. Specify a form.");if(!n.is("form, div")&&!n.attr("data-editable-form"))throw new Error('In case of Complex Editable type, the form\'s id should match the "data-editable-form" attribute on ul tag, and the form element itself should have data-editable-form="true" attribute.');n=n.detach()}catch(a){console.error(a.message)}}return n},_getUnenhancedList:function(t){return t.removeClass("ui-listview ui-shadow ui-corner-all ui-listview-inset ui-group-theme-"+this.options.theme).find("li").removeClass("ui-li-static ui-body-inherit ui-first-child ui-last-child ui-li-has-alt").end().find("a").removeClass("ui-link").end()},_afterListviewRefresh:function(){this.options.editable&&this._attachDetachEventHandlers()},_wrapCollapsible:function(){var t=this.element;return t.wrap("
    ").before(this._$markup.header(this.options,this._isListEmpty())),t.closest(":jqmData(role='collapsible')").collapsible()},_attachEditEventButtons:function(){this._isListEmpty()&&this._ui.header.off("click"),this._on(this._ui.button,{click:"_onEditButtonTapped"})},_onEditButtonTapped:function(t){t.preventDefault(),t.stopPropagation();var e=this._editMode=!this._editMode,i=this.element.parents(":jqmData(role='collapsible')");i.collapsible(e?"expand":"collapse"),this.refresh(),e||this._triggerListChange(t)},_updateHeader:function(){var t=this._ui,e=this.options,i=this._isListEmpty();t.header.children("a")[0].childNodes[0].data=i?e.emptyTitle:e.title,t.button.removeClass("ui-icon-minus ui-icon-"+e.doneIcon+" ui-icon-"+e.addIcon+" ui-icon-"+e.editIcon).addClass("ui-icon-"+(this._editMode?e.doneIcon:i?e.addIcon:e.editIcon)).text(this._editMode?e.doneLabel:i?e.addLabel:e.editLabel)},_triggerListChange:function(t){var e=this.options;this._trigger("change",t,{items:"simple"===e.editableType&&""!==e.itemName?this._toObjectCollection(this._toArray(this._items),e.itemName):this._toArray(this._items),added:"simple"===e.editableType&&""!==e.itemName?this._toObjectCollection(this._newItems,e.itemName):this._newItems,length:this.length()}),this._newItems=[]},_attachDetachEventHandlers:function(){this._enableInsertListItemEvent(),this._enableListItemDeleteEvent()},_enableInsertListItemEvent:function(){var t,e,i,n=this.options,a=n.editableType,l=this._ui.content;this._editMode&&(t=l.find("simple"===a?"li.ui-editable-temp a#item-add":"li:first-child [data-add-button='true']"),e="complex"===a?l.find("li:first-child [data-clear-button='true']"):null,i="simple"===a?l.find("input[type=text]"):null,this._off(t,"tap"),this._on(t,{tap:"_insertListItem"}),null!==e&&(this._off(e,"tap"),this._on(e,{tap:"_clearTextFields"})),null!==i&&(this._off(i,"keyup"),this._on(i,{keyup:"_insertListItem"})))},_clearTextFields:function(e){e.preventDefault();var i=t(e.target).parents("li").find("[data-item-name]");t.each(i,function(e,i){t(i).val("")})},_enableListItemDeleteEvent:function(){this._editMode?this._on(this._ui.content.find("a.ui-editable-btn-del"),{click:"_deleteListItem"}):this._off(this._ui.content.find("a.ui-editable-btn-del"),"click")},_insertListItem:function(e){e.preventDefault();var i=this.element,n={};if("tap"===e.type||e.keyCode===t.mobile.keyCode.ENTER){if("complex"===this.options.editableType){var a="",l=!0,s=t(e.target).parents("li").find("[data-item-name]");if(t.each(s,function(e,s){var o=t(s),d=o.data("item-template"),r=o.attr("type"),h=o.data("item-name"),u=null;switch(r){case"text":case"number":u=o.val(),n[h]=u,o.val("");break;case"checkbox":u=o.is(":checked"),n[h]=u;break;case"radio":var h=o.attr("name"),c=i.find("li:first-child input[data-item-name='"+h+"']").filter(":radio");c.each(function(){var e=t(this);e.is(":checked")&&(u=e.data("item-display-value"),n[h]=e.val())})}u||"checkbox"===r||(l=!1);var p=d.replace(/%%/,u);a+=-1===a.indexOf(p)?p:""}),!l)return;var o=this._counter;this._counter++,this._newItems.push(n),this._items[o]=n,a=t("
  • "+a+"
  • "),a.attr("data-"+this._dataItemName,o),this._origDom.prepend(a),this.refresh()}if("simple"===this.options.editableType){var d=t(e.target),r="keyup"===e.type?d:d.prev().find("input"),h=r.val();if(h){r.val("");var o=this._counter;this._counter++,this._newItems.push(h),this._items[o]=h;var a=this._isListEmpty()?t("
  • "):this._origDom.find("li").first().clone();a.attr("data-"+this._dataItemName,o),0===a.children().length?a.text(h):a.children("a").text(h),this._origDom.prepend(a),this.refresh()}}}},_deleteListItem:function(e){e.preventDefault(),e.stopPropagation();var i=t(e.currentTarget).parent(),n=i.data(this._dataItemName);this._origDom.find("li[data-"+this._dataItemName+'="'+n+'"]').remove(),delete this._items[n],i.remove(),this._updateHeader()},_isListEmpty:function(){return 0===this.element.find("li").not("li.ui-editable-temp").length?!0:!1},_$markup:{header:function(t){return"

    "+t.title+"

    "},listTextInput:"
  • "},_toArray:function(t){for(var e=[],i=Object.keys(t),n=0;n .ui-btn { 4 | cursor: pointer; 5 | -webkit-border-radius: .3125em; 6 | border-radius: .3125em; 7 | } 8 | /* --- End of Patch ---- */ 9 | .ui-collapsible-heading.ui-header > h1,h2,h3,h4,h5,h6 { 10 | text-align: left; 11 | margin-left: 40px; 12 | } 13 | .ui-editable-flex { 14 | width: 100%; 15 | display: inline-flex; 16 | flex-direction: row; 17 | } 18 | .ui-editable-border-right { 19 | border-top-right-radius: .3125em; 20 | border-bottom-right-radius: .3125em; 21 | } 22 | .ui-editable-border-left { 23 | border-top-left-radius: .3125em; 24 | border-bottom-left-radius: .3125em; 25 | } 26 | .ui-editable-flex-item-right { 27 | flex: 1 1 auto; 28 | } 29 | /* Large desktop */ 30 | @media (min-width: 1200px) { 31 | .ui-editable-flex-item-left { 32 | flex: 35 1 auto; 33 | } 34 | } 35 | 36 | /* Landscape tablet and dated desktop */ 37 | @media (min-width: 980px) and (max-width: 1199px) { 38 | .ui-editable-flex-item-left { 39 | flex: 25 1 auto; 40 | } 41 | } 42 | 43 | /* Portrait tablet to landscape and desktop */ 44 | @media (min-width: 767px) and (max-width: 979px) { 45 | .ui-editable-flex-item-left { 46 | flex: 15 1 auto; 47 | } 48 | } 49 | 50 | /* Landscape phone to portrait tablet */ 51 | @media (max-width: 767px) { 52 | .ui-editable-flex-item-left { 53 | flex: 15 1 auto; 54 | } 55 | } 56 | 57 | /* Landscape phones and down */ 58 | @media (max-width: 480px) { 59 | .ui-editable-flex-item-left { 60 | flex: 5 1 auto; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baig/jquerymobile-editablelistview/63bcf987cfe88c2e917e248cbb213f5afc30390c/demo/demo.css -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jQM Editable Listview Demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
    26 |

    Delete "Orange" from the following list of "Fruits"

    27 |
      28 |
    • Apple
    • 29 |
    • Orange
    • 30 |
    • Banana
    • 31 |
    • Mango
    • 32 |
    33 | 34 |
    35 | 36 |

    Add "Running" and "Surfing" in the following list

    37 |
      38 |
    • Running
    • 39 |
    • Cycling
    • 40 |
    • Surfing
    • 41 |
    • Jogging
    • 42 |
    43 | 44 |
    45 | 46 |

    Delete "Orange" from the following list of "Fruits"

    47 |
      48 |
    • Apple
    • 49 |
    • Orange
    • 50 |
    • Banana
    • 51 |
    • Mango
    • 52 |
    53 |

    Press the button below to programmatically insert a list item

    54 | 55 | 56 |
    57 | 58 |

    An example of Complex Editable Listview

    59 | 82 | 83 |
    84 | 85 | 86 | 87 | 88 | 89 |
    90 | 91 |
    92 | 93 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /editable-listview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baig/jquerymobile-editablelistview/63bcf987cfe88c2e917e248cbb213f5afc30390c/editable-listview.png -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * gulp [Task] 3 | * 4 | * Available Build Task(s) 5 | * - clean 6 | * - concat 7 | * - minify 8 | * - minify-css 9 | * - build 10 | */ 11 | 12 | var gulp = require('gulp'); 13 | var help = require('gulp-help'); 14 | var concat = require('gulp-concat'); 15 | var uglify = require('gulp-uglify'); 16 | var minifyCss = require('gulp-minify-css'); 17 | var rename = require('gulp-rename'); 18 | var del = require('del'); 19 | var noop = function () {}; 20 | 21 | //var usemin = require('gulp-usemin'); 22 | //var sourcemaps = require('gulp-sourcemaps'); 23 | 24 | /** gulp tasks */ 25 | 26 | help(gulp); 27 | 28 | // Clean 29 | gulp.task('clean', 'Cleans the build folder.', [], function (cb) { 30 | "use strict"; 31 | 32 | del([ 33 | 'build/**' 34 | ], cb); 35 | 36 | }, { 37 | aliases: ['c', 'C'] 38 | }); 39 | 40 | gulp.task("concat", 'Joins all the script files putting them in build folder.', [], function () { 41 | "use strict"; 42 | gulp.src(['js/editable-listview.js', 'js/**/*.js']) 43 | .pipe(concat('jqm.editable.listview.js')) 44 | .pipe(gulp.dest('build/')); 45 | }, { 46 | aliases: ['j', 'J'] 47 | }); 48 | 49 | gulp.task("minify", 'Minifies all the script files.', [], function () { 50 | "use strict"; 51 | gulp.src(['js/editable-listview.js', 'js/**/*.js']) 52 | .pipe(concat('jqm.editable.listview.js')) 53 | .pipe(rename('jqm.editable.listview.min.js')) 54 | .pipe(uglify({ 55 | preserveComments: 'some' 56 | })) 57 | .pipe(gulp.dest('build/')); 58 | }, { 59 | aliases: ['m', 'M'] 60 | }); 61 | 62 | gulp.task("minify-css", 'Minifies the CSS stylesheets.', [], function () { 63 | "use strict"; 64 | gulp.src('css/**/*.css') 65 | .pipe(concat('jqm.editable.listview.min.css')) 66 | .pipe(minifyCss({ 67 | noAdvanced: false 68 | })) 69 | .pipe(gulp.dest('build')); 70 | }, { 71 | aliases: ['s', 'S'] 72 | }); 73 | 74 | gulp.task("assets", 'Copies all assets (css stylesheets, images etc.) to the build folder.', [], function () { 75 | "use strict"; 76 | gulp.src("css/**/*") 77 | .pipe(concat("jqm.editable.listview.css")) 78 | .pipe(gulp.dest('build')); 79 | }, { 80 | aliases: ['a', 'A'] 81 | }); 82 | 83 | gulp.task("build", '(default task) Cleans, concatenates and minifies all script files into build folder.', [ 84 | 'clean', 85 | 'concat', 86 | 'minify', 87 | 'assets', 88 | 'minify-css' 89 | ], noop, { 90 | aliases: ['b', 'B'] 91 | }); 92 | 93 | gulp.task('lint', '', [], function () { 94 | /* style and lint errors */ 95 | var jscs = require('gulp-jscs'); 96 | var jshint = require('gulp-jshint'); 97 | var stylish = require('gulp-jscs-stylish'); 98 | 99 | gulp.src('js/*.js') 100 | .pipe(jshint()) // hint 101 | .pipe(jscs()) // enforce style guide 102 | .on('error', noop) // don't stop on error 103 | .pipe(stylish.combineWithHintResults()) // combine with jshint results 104 | .pipe(jshint.reporter('jshint-stylish')); // use any jshint reporter to log hint and style guide errors 105 | }); 106 | 107 | gulp.task('unit', '', [], function () { 108 | var mochaPhantomJS = require('gulp-mocha-phantomjs'); 109 | 110 | return gulp.src(['tests/test-runner.html'], { 111 | read: false 112 | }) 113 | .pipe(mochaPhantomJS({ 114 | reporter: 'spec' 115 | })); 116 | }); 117 | 118 | 119 | gulp.task('default', '', ['build']); 120 | 121 | gulp.task('test', ['lint', 'unit']); 122 | -------------------------------------------------------------------------------- /js/editable-listview.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Mobile Editable Listview Plugin 3 | * https://github.com/baig/jquerymobile-editablelistview 4 | * 5 | * Copyright 2014 (c) Wasif Hasan Baig 6 | * 7 | * Released under the MIT license 8 | * https://github.com/baig/jquerymobile-editablelistview/blob/master/LICENSE.txt 9 | */ 10 | 11 | ( function( $, undefined ) { 12 | 13 | "use strict"; 14 | 15 | $.widget( "mobile.listview", $.mobile.listview, { 16 | 17 | // private variables 18 | _evt: null, 19 | _counter: 1, 20 | _created: false, 21 | _origDom: null, 22 | _editMode: false, 23 | _tapHandler: null, 24 | _clickHandler: null, 25 | _dataItemName: "item", 26 | 27 | // options hash 28 | options: { 29 | // the essential ones 30 | editable: false, 31 | itemName: "", 32 | editableType: "simple", 33 | editableForm: "", 34 | 35 | title: "View list items", // header title when listview has list items 36 | emptyTitle: "No items to view", // header title when listview is empty 37 | 38 | collapsed: false, 39 | expandedIcon: "carat-d", 40 | collapsedIcon: "carat-r", 41 | 42 | addLabel: "Add", 43 | editLabel: "Edit", 44 | doneLabel: "Done", 45 | 46 | addIcon: "plus", 47 | editIcon: "edit", 48 | doneIcon: "check", 49 | itemIcon: false, 50 | 51 | buttonTheme: "a", 52 | buttonCorner: true, 53 | buttonShadow: true 54 | }, 55 | 56 | _beforeListviewRefresh: function() { 57 | 58 | // Returning immediately if `data-editable="false"` 59 | if ( !this.options.editable ) { 60 | return; 61 | } 62 | 63 | var $el = this.element, 64 | opts = this.options, 65 | $origDom = this._origDom, 66 | dataItemName = this._dataItemName, 67 | counter = this._counter, 68 | ui = {}, 69 | $orig, 70 | $origLis, 71 | liLength, 72 | evt = this._evt, 73 | $lis = $el.find( "li" ), 74 | $markup = this._$markup; 75 | 76 | /** 77 | * saving original DOM structure if there is a discrepency in the number 78 | * of list items between `this.element` and `this._origDom` or if the 79 | * or if `this._origDom` is null 80 | * Note: list item length count ignores the list item housing the 81 | * text box 82 | * Fix for Bug #4 83 | */ 84 | // ## Creation 85 | if ( !this._created ) { 86 | liLength = $lis.not( "li.ui-editable-temp" ).length; 87 | if ( liLength !== ( $origDom === null ? -1 : $origDom.find( "li" ).length ) ) { 88 | $origDom = $el.clone(); 89 | // Assign each list item a unique number value 90 | $.each( $origDom.children( "li" ), function( idx, val ) { 91 | $( val ).attr( "data-" + dataItemName, counter ); 92 | counter++; 93 | } ); 94 | // incrementing the counter that is used to assign unique 95 | // value to `data-item` attribute on each list item 96 | this._counter = counter; 97 | // Caching the original list to the widget instance 98 | this._origDom = this._getUnenhancedList( $origDom ); 99 | } 100 | 101 | // Wrapping the list structure inside Collapsible 102 | ui.wrapper = this._wrapCollapsible(); 103 | ui.header = ui.wrapper.find( ".ui-collapsible-heading" ); 104 | ui.button = ui.header.find( "a a, a button" ); 105 | ui.content = ui.wrapper.find( ".ui-collapsible-content" ); 106 | ui.form = this._getForm(); 107 | 108 | $.extend( this, { 109 | _ui: ui, 110 | _newItems: [], 111 | _items: {} 112 | } ); 113 | 114 | this._items = this._getExistingListContents(); 115 | 116 | evt = this._evt = $._data( ui.header[0], "events" ); 117 | this._clickHandler = evt.click[0].handler; 118 | this._tapHandler = evt.tap[0].handler; 119 | 120 | this._attachEditEventButtons(); 121 | 122 | this._created = true; 123 | } 124 | 125 | if ( this._editMode ) { 126 | 127 | ui = this._ui; 128 | $orig = $origDom.clone(); 129 | $origLis = $orig.find( "li" ); 130 | 131 | if ( $orig.find( "li.ui-editable-temp" ).length === 0 ) { 132 | // Checking if text content of
  • is wrapped inside 133 | if ( $orig.find( "a" ).length === 0 ) { 134 | // wrapping contents of
  • inside 135 | $origLis.wrapInner( "" ); 136 | } 137 | 138 | // appending another inside
  • ; this is the delete button 139 | $origLis.append( "Delete" ); 140 | 141 | this.option( "splitIcon", "minus" ); 142 | 143 | if ( opts.editableType === "complex" ) { 144 | $orig.prepend( "
  • " ); 145 | $orig.find( "li.ui-editable-temp" ).append( ui.form ); 146 | } 147 | if ( opts.editableType === "simple" ) { 148 | $orig.prepend( $markup.listTextInput ); 149 | } 150 | } 151 | 152 | $lis.remove(); 153 | 154 | $el.append( $orig.find( "li" ) ); 155 | 156 | /** 157 | * Disabling the click and tap event handlers on header when the 158 | * list is in `Edit` mode 159 | */ 160 | evt.click[0].handler = evt.tap[0].handler = function( e ) { 161 | e.stopPropagation(); 162 | e.preventDefault(); 163 | }; 164 | } else { 165 | 166 | // Re-enabling the click event handler when the list is in `View` mode 167 | evt.click[0].handler = this._clickHandler; 168 | evt.tap[0].handler = this._tapHandler; 169 | 170 | // Removing `Edit` mode `Li`s 171 | $lis.filter( ".ui-editable-temp" ).hide(); 172 | $lis.not( ".ui-editable-temp" ).remove(); 173 | 174 | if ( opts.itemIcon ) { 175 | $el.append( $origDom.clone().find( "li" ) ); 176 | } else { 177 | $el.append( $origDom.clone().find( "li" ).attr( "data-icon", "false" ) ); 178 | } 179 | } 180 | 181 | // Updating the header title, header button label and icon based on 182 | // the list contents and its state (`Edit` or `View`) 183 | this._updateHeader(); 184 | }, 185 | 186 | _getExistingListContents: function() { 187 | var opts = this.options, 188 | ui = this._ui, 189 | origDom = this._origDom, 190 | items = {}, 191 | tmpObj = {}; 192 | 193 | if ( this.options.editableType === "simple" ) { 194 | origDom.find( "li" ).each( function( idx, li ) { 195 | var $li = $( li ); 196 | items[$li.data( "item" )] = $li.text(); 197 | } ); 198 | } 199 | 200 | if ( opts.editableType === "complex" ) { 201 | 202 | ui.form.find( "input" ).each( function( idx, input ) { 203 | var dataItemName = $( input ).data( "item-name" ); 204 | tmpObj[dataItemName] = dataItemName; 205 | } ); 206 | 207 | origDom.find( "li" ).each( function( idx, li ) { 208 | var $li = $( li ), 209 | obj = {}, 210 | $span, itemValue; 211 | 212 | $.each( tmpObj, function( dataItemName ) { 213 | $span = $li.find( "span#" + dataItemName ); 214 | itemValue = $span.data( "value" ); 215 | if ( itemValue !== undefined ) { 216 | obj[dataItemName] = itemValue; 217 | } else { 218 | obj[dataItemName] = $span.text(); 219 | } 220 | } ); 221 | 222 | items[$li.data( "item" )] = obj; 223 | } ); 224 | } 225 | 226 | return items; 227 | }, 228 | 229 | _getForm: function() { 230 | var $el = this.element, 231 | opts = this.options, 232 | ui = this._ui, 233 | form = null; 234 | 235 | if ( opts.editableType === "complex" ) { 236 | if ( ui && ui.form ) { 237 | return ui.form; 238 | } else { 239 | try { 240 | if ( opts.editableForm.length === 0 ) { 241 | throw new Error( "Form not specified for the Complex " + 242 | "Editable Listview type." ); 243 | } 244 | form = $el.closest( ":jqmData(role='page')" ) 245 | .find( "#" + opts.editableForm ); 246 | if ( !form.length ) { 247 | throw new Error( "No form found. Specify a form." ); 248 | } 249 | if ( !form.is( "form, div" ) && !form.attr( "data-editable-form" ) ) { 250 | throw new Error( "In case of Complex Editable type, the form's " + 251 | "id should match the \"data-editable-form\" " + 252 | "attribute on ul tag, and the form element itself " + 253 | "should have data-editable-form=\"true\" " + 254 | "attribute." ); 255 | } 256 | form = form.detach(); 257 | } catch ( error ) { 258 | console.error( error.message ); 259 | } 260 | } 261 | } 262 | 263 | return form; 264 | }, 265 | 266 | _getUnenhancedList: function( $dom ) { 267 | // removing all CSS classes to get the original list structure 268 | return $dom 269 | .removeClass( "ui-listview ui-shadow ui-corner-all ui-listview-inset " + 270 | "ui-group-theme-" + this.options.theme ) 271 | .find( "li" ) 272 | .removeClass( "ui-li-static ui-body-inherit ui-first-child ui-last-child " + 273 | "ui-li-has-alt" ) 274 | .end() 275 | .find( "a" ) 276 | .removeClass( "ui-link" ) 277 | .end(); 278 | }, 279 | 280 | _afterListviewRefresh: function() { 281 | if ( this.options.editable ) { 282 | this._attachDetachEventHandlers(); 283 | } 284 | }, 285 | 286 | _wrapCollapsible: function() { 287 | var $el = this.element; 288 | 289 | $el.wrap( "
    " ) 290 | .before( this._$markup.header( this.options, this._isListEmpty() ) ); 291 | 292 | return $el.closest( ":jqmData(role='collapsible')" ).collapsible(); 293 | }, 294 | 295 | _attachEditEventButtons: function() { 296 | if ( this._isListEmpty() ) { 297 | this._ui.header.off( "click" ); 298 | } 299 | 300 | this._on( this._ui.button, { 301 | "click": "_onEditButtonTapped" 302 | } ); 303 | }, 304 | 305 | // --(start)-- Event Handlers -- 306 | 307 | _onEditButtonTapped: function( e ) { 308 | e.preventDefault(); 309 | e.stopPropagation(); 310 | 311 | var editMode = this._editMode = !this._editMode, 312 | $collapsible = this.element.parents( ":jqmData(role='collapsible')" ); 313 | 314 | editMode ? 315 | $collapsible.collapsible( "expand" ) : 316 | $collapsible.collapsible( "collapse" ); 317 | 318 | this.refresh(); 319 | 320 | if ( !editMode ) { 321 | this._triggerListChange( e ); 322 | } 323 | }, 324 | 325 | _updateHeader: function() { 326 | var ui = this._ui, 327 | opts = this.options, 328 | isListEmpty = this._isListEmpty(); 329 | 330 | // updating list header title 331 | ui.header 332 | .children( "a" )[0] 333 | .childNodes[0] 334 | .data = ( isListEmpty ) ? opts.emptyTitle : opts.title; 335 | 336 | // changing "Edit" button state, icon and label 337 | ui.button 338 | .removeClass( "ui-icon-minus ui-icon-" + opts.doneIcon + " " + 339 | "ui-icon-" + opts.addIcon + " " + 340 | "ui-icon-" + opts.editIcon ) 341 | .addClass( "ui-icon-" + 342 | this._editMode ? opts.doneIcon 343 | : isListEmpty ? opts.addIcon 344 | : opts.editIcon ) 345 | .text( this._editMode ? opts.doneLabel 346 | : isListEmpty ? opts.addLabel 347 | : opts.editLabel ); 348 | 349 | }, 350 | 351 | // _triggerListChange 352 | _triggerListChange: function( e ) { 353 | var opts = this.options, 354 | items = this._items, 355 | newItems = this._newItems; 356 | 357 | this._trigger( "change", e, { 358 | items: ( opts.editableType === "simple" && opts.itemName !== "" ) ? 359 | this._toObjectCollection( this._toArray( items ), opts.itemName ) : 360 | this._toArray( this._items ), 361 | added: ( opts.editableType === "simple" && opts.itemName !== "" ) ? 362 | this._toObjectCollection( newItems, opts.itemName ) : 363 | this._newItems, 364 | length: this.length() 365 | } ); 366 | 367 | // emptying the _newItems array 368 | this._newItems = []; 369 | }, 370 | 371 | // --(end)-- Event Handlers -- 372 | 373 | // --(start)-- Event Handler Helper Functions -- 374 | 375 | _attachDetachEventHandlers: function() { 376 | this._enableInsertListItemEvent(); 377 | this._enableListItemDeleteEvent(); 378 | 379 | // this._enableListItemEditing() // v0.3 380 | }, 381 | 382 | _enableInsertListItemEvent: function() { 383 | var $addBtn, $clearBtn, $textField, 384 | opts = this.options, 385 | editableType = opts.editableType, 386 | $content = this._ui.content; 387 | 388 | if ( this._editMode ) { 389 | $addBtn = ( editableType === "simple" ) ? 390 | $content.find( "li.ui-editable-temp a#item-add" ) : 391 | $content.find( "li:first-child [data-add-button='true']" ), 392 | $clearBtn = ( editableType === "complex" ) ? 393 | $content.find( "li:first-child [data-clear-button='true']" ) : 394 | null, 395 | $textField = ( editableType === "simple" ) ? 396 | $content.find( "input[type=text]" ) : 397 | null; 398 | 399 | this._off( $addBtn, "tap" ); 400 | this._on( $addBtn, { 401 | "tap": "_insertListItem" 402 | } ); 403 | 404 | if ( $clearBtn !== null ) { 405 | this._off( $clearBtn, "tap" ); 406 | this._on( $clearBtn, { 407 | "tap": "_clearTextFields" 408 | } ); 409 | } 410 | 411 | if ( $textField !== null ) { 412 | this._off( $textField, "keyup" ); 413 | this._on( $textField, { 414 | "keyup": "_insertListItem" 415 | } ); 416 | } 417 | } 418 | }, 419 | 420 | _clearTextFields: function( e ) { 421 | e.preventDefault(); 422 | 423 | var inputs = $( e.target ).parents( "li" ).find( "[data-item-name]" ); 424 | 425 | $.each( inputs, function( idx, val ) { 426 | $( val ).val( "" ); 427 | } ); 428 | }, 429 | 430 | _enableListItemDeleteEvent: function() { 431 | var $delBtn = this._ui.content.find( "a.ui-editable-btn-del" ); 432 | this._editMode ? 433 | this._on( $delBtn, { "click": "_deleteListItem" } ) : 434 | this._off( $delBtn, "click" ); 435 | }, 436 | 437 | // TODO v0.3 438 | /*_enableListItemEditing: function() {},*/ 439 | 440 | _insertListItem: function( e ) { 441 | e.preventDefault(); 442 | 443 | var $el = this.element, 444 | itemObj = {}, 445 | liTemplate = "", 446 | dataItemNumber = 0, 447 | proceed = true, 448 | $input, $inputs, $target, inputTextString, dataItemSelector, 449 | $radios, renderedTemplate; 450 | 451 | // returning immediately if keyup keycode does not match keyCode.ENTER i.e. 13 452 | if ( e.type !== "tap" && e.keyCode !== $.mobile.keyCode.ENTER ) { 453 | return; 454 | } 455 | 456 | if ( this.options.editableType === "complex" ) { 457 | 458 | $inputs = $( e.target ).parents( "li" ).find( "[data-item-name]" ); 459 | 460 | $.each( $inputs, function( idx, val ) { 461 | var $input = $( val ), 462 | template = $input.data( "item-template" ), 463 | inputType = $input.attr( "type" ), 464 | itemName = $input.data( "item-name" ), 465 | value = null; 466 | 467 | switch ( inputType ) { 468 | case "text": 469 | case "number": 470 | value = $input.val(); 471 | itemObj[itemName] = value; 472 | $input.val( "" ); 473 | break; 474 | case "checkbox": 475 | value = $input.is( ":checked" ); 476 | itemObj[itemName] = value; 477 | break; 478 | case "radio": 479 | dataItemSelector = "li:first-child " + 480 | "input[data-item-name='" + 481 | itemName + "']"; 482 | $radios = $el.find( dataItemSelector ) 483 | .filter( ":radio" ); 484 | itemName = $input.attr( "name" ), 485 | $radios.each( function() { 486 | var $this = $( this ); 487 | if ( $this.is( ":checked" ) ) { 488 | value = $this.data( "item-display-value" ); 489 | itemObj[itemName] = $this.val(); 490 | } 491 | } ); 492 | break; 493 | } 494 | 495 | if ( !value && inputType !== "checkbox" ) { 496 | proceed = false; 497 | } 498 | 499 | renderedTemplate = template.replace( /%%/, value ); 500 | 501 | liTemplate += ( liTemplate.indexOf( renderedTemplate ) === -1 ) ? 502 | renderedTemplate : // Add only if not already present 503 | ""; // Skip if value already present in liTemplate 504 | } ); 505 | 506 | // Not proceeding to add if any input value is empty 507 | if ( !proceed ) { 508 | return; 509 | } 510 | 511 | dataItemNumber = this._counter; 512 | this._counter++; 513 | 514 | this._newItems.push( itemObj ); 515 | this._items[dataItemNumber] = itemObj; 516 | 517 | liTemplate = $( "
  • " + liTemplate + "
  • " ); 518 | liTemplate.attr( "data-" + this._dataItemName, dataItemNumber ); 519 | 520 | this._origDom.prepend( liTemplate ); 521 | 522 | this.refresh(); 523 | 524 | } 525 | 526 | if ( this.options.editableType === "simple" ) { 527 | 528 | $target = $( e.target ); 529 | $input = ( e.type === "keyup" ) ? 530 | $target : 531 | $target.prev().find( "input" ); 532 | inputTextString = $input.val(); 533 | 534 | // Inserting list item only if input string is not empty 535 | if ( !!inputTextString ) { 536 | $input.val( "" ); // Clearing the input text field 537 | 538 | dataItemNumber = this._counter; 539 | this._counter++; 540 | 541 | this._newItems.push( inputTextString ); 542 | this._items[dataItemNumber] = inputTextString; 543 | 544 | liTemplate = this._isListEmpty() ? 545 | // simple static list template if list is empty 546 | $( "
  • " ) : 547 | // copying existing list structure if list is not empty 548 | this._origDom 549 | .find( "li" ) 550 | .first() 551 | .clone(); 552 | 553 | liTemplate.attr( "data-" + this._dataItemName, dataItemNumber ); 554 | 555 | if ( liTemplate.children().length === 0 ) { 556 | liTemplate.text( inputTextString ); 557 | } else { 558 | liTemplate.children( "a" ) 559 | .text( inputTextString ); 560 | } 561 | 562 | this._origDom.prepend( liTemplate ); 563 | 564 | this.refresh(); 565 | } 566 | } 567 | }, 568 | 569 | _deleteListItem: function( e ) { 570 | e.preventDefault(); 571 | e.stopPropagation(); 572 | 573 | var $parentTarget = $( e.currentTarget ).parent(), 574 | itemNum = $parentTarget.data( this._dataItemName ); 575 | 576 | this._origDom 577 | .find( "li[data-" + this._dataItemName + "=\"" + itemNum + "\"]" ) 578 | .remove(); 579 | 580 | delete this._items[itemNum]; 581 | 582 | $parentTarget.remove(); 583 | 584 | this._updateHeader(); 585 | }, 586 | 587 | // --(end)-- Event Handler Helper Functions -- 588 | 589 | /* 590 | _destroy: function() { 591 | var ui = this._ui, 592 | opts = this.options, 593 | $ul = ui.content.filter( "ul" ), 594 | $li = $ul.find( "li" ), 595 | items = this.items() 596 | 597 | // Not doing anything if DOM was already enhanced 598 | if ( opts.enhanced ) { 599 | return this; 600 | } 601 | 602 | ui.header.remove() 603 | ui.content = ui.content.unwrap().unwrap() 604 | 605 | $ul.removeClass( "ui-listview ui-corner-all ui-shadow ui-collapsible-collapsed" ) 606 | $ul.find( "a" ).remove() 607 | this._removeFirstLastClasses($li) 608 | $li.removeClass( "ui-li-has-alt" ) 609 | $li.each( function(idx, val) { 610 | this.textContent = items[idx] 611 | }) 612 | 613 | return ui 614 | },*/ 615 | 616 | _isListEmpty: function() { 617 | var length = this.element 618 | .find( "li" ) 619 | .not( "li.ui-editable-temp" ) 620 | .length; 621 | return length === 0; 622 | }, 623 | 624 | _$markup: { 625 | 626 | header: function( opts ) { 627 | var btnClasses = "ui-btn ui-mini ui-btn-inline ui-btn-right ui-btn-icon-right"; 628 | return "

    " + opts.title + 629 | "" + 636 | "

    "; 637 | }, 638 | 639 | listTextInput: 640 | "
  • " + 641 | "
    " + 642 | "
    " + 645 | "" + 646 | "
    " + 647 | "" + 651 | "Add" + 652 | "" + 653 | "
    " + 654 | "
  • " 655 | 656 | }, 657 | 658 | _toArray: function( obj ) { 659 | var arr = [], 660 | keys = Object.keys( obj ), 661 | i = 0; 662 | 663 | for ( ; i < keys.length; i++ ) { 664 | arr.push( obj[keys[i]] ); 665 | } 666 | 667 | return arr; 668 | }, 669 | 670 | _toObjectCollection: function( arr, keyName ) { 671 | var arrOfObj = [], 672 | obj = {}, 673 | i = 0; 674 | 675 | for ( ; i < arr.length; i++ ) { 676 | obj[keyName] = arr[i]; 677 | arrOfObj.push( obj ); 678 | } 679 | 680 | return arrOfObj; 681 | }, 682 | 683 | // Public API 684 | 685 | length: function() { 686 | return this.element 687 | .find( "li" ) 688 | .not( ".ui-editable-temp" ) 689 | .length; 690 | }, 691 | 692 | items: function() { 693 | // stringifying and parsing to returned a cloned copy with no internal object references 694 | return JSON.parse( JSON.stringify( this._items ) ); 695 | }, 696 | 697 | widget: function() { 698 | return this._ui.wrapper; 699 | } 700 | 701 | } ); 702 | 703 | }( jQuery ) ); 704 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jqm-editable-listview", 3 | "version": "0.3.1", 4 | "description": "A customized version of jQuery Mobile Listview Widget that supports insertion of new list items and removal of existing list items", 5 | "main": "js/editable-listview.js", 6 | "scripts": { 7 | "test": "./node_modules/karma/bin/karma start --browsers Firefox --single-run" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/baig/jquerymobile-editablelistview.git" 12 | }, 13 | "keywords": [ 14 | "jquery", 15 | "mobile", 16 | "jquerymobile", 17 | "jqm", 18 | "listview", 19 | "list", 20 | "editable", 21 | "editablelistview", 22 | "editable-listview" 23 | ], 24 | "author": "Wasif Hasan Baig", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/baig/jquerymobile-editablelistview/issues" 28 | }, 29 | "scripts" : { 30 | "test": "casperjs test tests/functional/casper.spec.js" 31 | }, 32 | "homepage": "https://github.com/baig/jquerymobile-editablelistview", 33 | "devDependencies": { 34 | "casper-chai": "^0.2.1", 35 | "chai": "^2.1.2", 36 | "chai-as-promised": "^4.3.0", 37 | "chai-jq": "0.0.8", 38 | "chai-jquery": "^2.0.0", 39 | "del": "^1.1.0", 40 | "gulp": "^3.8.11", 41 | "gulp-concat": "^2.4.2", 42 | "gulp-help": "^1.3.1", 43 | "gulp-jscs": "^1.4.0", 44 | "gulp-jscs-stylish": "^1.0.2", 45 | "gulp-jshint": "^1.9.4", 46 | "gulp-minify-css": "^0.3.11", 47 | "gulp-mocha": "^2.0.0", 48 | "gulp-mocha-phantomjs": "^0.5.3", 49 | "gulp-rename": "^1.2.0", 50 | "gulp-uglify": "^1.0.2", 51 | "jquery": "^2.1.3", 52 | "jquery-mobile": "^1.4.1", 53 | "jshint-stylish": "^1.0.1", 54 | "mocha": "^2.2.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/functional/casper.spec.js: -------------------------------------------------------------------------------- 1 | var casper = require('casper').create(); 2 | var cwd = require('fs').workingDirectory; 3 | var dump = require('utils').dump; 4 | var system = require('system'); 5 | var webpage = require('webpage'); 6 | 7 | casper.test.setUp(function () { 8 | casper.options.clientScripts = [ 9 | './node_modules/jquery/dist/jquery.min.js', 10 | './node_modules/jquery-mobile/dist/jquery.mobile.min.js', 11 | './js/editable-listview.js' 12 | ]; 13 | // casper.start().userAgent('Mosaic 0.1'); 14 | casper.start("file://" + cwd + "/tests/functional/simple.html"); 15 | }); 16 | 17 | casper.test.tearDown(function () { 18 | casper.echo('Goodbye!'); 19 | }); 20 | 21 | casper.test.begin('Simple Editable Listview is enhanced with correct markup', 5, function suite(test) { 22 | 23 | casper.then(function () { 24 | dump(this.getElementAttribute('h1 button', 'class')) 25 | test.assertEquals(this.exists('div[data-role="collapsible"]'), true); 26 | test.assertEquals(this.getElementAttribute('div[data-role="collapsible"]', 'class'), 'ui-collapsible ui-collapsible-inset ui-corner-all ui-collapsible-themed-content ui-collapsible-collapsed'); 27 | test.assertEquals(this.getElementAttribute('h1', 'class'), 'ui-collapsible-heading ui-collapsible-heading-collapsed'); 28 | test.assertEquals(this.exists('h1.ui-collapsible-heading'), true); 29 | test.assertEquals(this.exists('h1.ui-collapsible-heading'), true); 30 | }); 31 | 32 | casper.run(function () { 33 | test.done(); 34 | }); 35 | }); 36 | 37 | casper.test.begin('Clicking on Add button takes into Edit mode', 1, function (test) { 38 | casper.then(function () { 39 | this.click('h1 button'); 40 | }); 41 | 42 | casper.then(function () { 43 | // console.log('clicked edit button') 44 | test.assertEquals(this.exists('ul li.ui-editable-temp'), true); 45 | // dump(this.getElementInfo('div[data-role="collapsible"]').html) 46 | }); 47 | 48 | casper.run(function () { 49 | test.done(); 50 | }); 51 | }); 52 | 53 | //casper.test.begin('Clicking on plus button inserts the list item', 6, function (test) { 54 | // casper.start("file://" + cwd + "/tests/functional/simple.html"); 55 | // 56 | // casper.then(function () { 57 | // test.assertTitle("Casper", "Casper title is ok"); 58 | // test.assertTitle("Casper", "Casper title is ok"); 59 | // test.assertTitle("Casper", "Casper title is ok"); 60 | // }); 61 | // 62 | // casper.run(function () { 63 | // test.done(); 64 | // }); 65 | //}); 66 | // 67 | //casper.test.begin('Nothing happens when clicking on plus button when input text is empty', 6, function (test) { 68 | // casper.start("file://" + cwd + "/tests/functional/simple.html"); 69 | // 70 | // casper.then(function () { 71 | // test.assertTitle("Casper", "Casper title is ok"); 72 | // test.assertTitle("Casper", "Casper title is ok"); 73 | // test.assertTitle("Casper", "Casper title is ok"); 74 | // }); 75 | // 76 | // casper.run(function () { 77 | // test.done(); 78 | // }); 79 | //}); 80 | // 81 | //casper.test.begin('Clicking on Done', 6, function (test) { 82 | // casper.start("file://" + cwd + "/tests/functional/simple.html"); 83 | // 84 | // casper.then(function () { 85 | // test.assertTitle("Casper", "Casper title is ok"); 86 | // test.assertTitle("Casper", "Casper title is ok"); 87 | // test.assertTitle("Casper", "Casper title is ok"); 88 | // }); 89 | // 90 | // casper.run(function () { 91 | // test.done(); 92 | // }); 93 | //}); 94 | // 95 | // 96 | // 97 | //casper.test.begin('ClientUtils.exists() tests', 5, function (test) { 98 | // var clientutils = require('clientutils').create(); 99 | // fakeDocument(''); 100 | // test.assert(clientutils.exists('ul'), 101 | // 'ClientUtils.exists() checks that an element exist'); 102 | // test.assertNot(clientutils.exists('ol'), 103 | // 'ClientUtils.exists() checks that an element exist'); 104 | // test.assert(clientutils.exists('ul.foo li'), 105 | // 'ClientUtils.exists() checks that an element exist'); 106 | // // xpath 107 | // test.assert(clientutils.exists(x('//ul')), 108 | // 'ClientUtils.exists() checks that an element exist using XPath'); 109 | // test.assertNot(clientutils.exists(x('//ol')), 110 | // 'ClientUtils.exists() checks that an element exist using XPath'); 111 | // fakeDocument(null); 112 | // test.done(); 113 | //}); 114 | // 115 | //casper.test.begin('ClientUtils.getElementBounds() tests', 3, function (test) { 116 | // casper.start().then(function () { 117 | // this.page.content = '
    '; 118 | // test.assertEquals( 119 | // this.getElementBounds('#b1'), { 120 | // top: 10, 121 | // left: 11, 122 | // width: 50, 123 | // height: 60 124 | // }, 125 | // 'ClientUtils.getElementBounds() retrieves element boundaries' 126 | // ); 127 | // }); 128 | // casper.then(function () { 129 | // var html = '
    '; 130 | // html += '
    '; 131 | // html += '
    '; 132 | // html += '
    '; 133 | // this.page.content = html; 134 | // var bounds = this.getElementsBounds('#boxes div'); 135 | // test.assertEquals( 136 | // bounds[0], { 137 | // top: 10, 138 | // left: 11, 139 | // width: 50, 140 | // height: 60 141 | // }, 142 | // 'ClientUtils.getElementsBounds() retrieves multiple elements boundaries' 143 | // ); 144 | // test.assertEquals( 145 | // bounds[1], { 146 | // top: 20, 147 | // left: 21, 148 | // width: 70, 149 | // height: 80 150 | // }, 151 | // 'ClientUtils.getElementsBounds() retrieves multiple elements boundaries' 152 | // ); 153 | // }); 154 | // casper.run(function () { 155 | // test.done(); 156 | // }); 157 | //}); 158 | // 159 | // 160 | //casper.test.begin('ClientUtils.getElementBounds() page zoom factor tests', 3, function (test) { 161 | // casper.start().zoom(2).then(function () { 162 | // var html = '
    '; 163 | // html += '
    '; 164 | // html += '
    '; 165 | // html += '
    '; 166 | // this.page.content = html; 167 | // test.assertEquals( 168 | // this.getElementBounds('#b1'), { 169 | // top: 20, 170 | // left: 22, 171 | // width: 100, 172 | // height: 120 173 | // }, 174 | // 'ClientUtils.getElementBounds() is aware of the page zoom factor' 175 | // ); 176 | // var bounds = this.getElementsBounds('#boxes div'); 177 | // test.assertEquals( 178 | // bounds[0], { 179 | // top: 20, 180 | // left: 22, 181 | // width: 100, 182 | // height: 120 183 | // }, 184 | // 'ClientUtils.getElementsBounds() is aware of the page zoom factor' 185 | // ); 186 | // test.assertEquals( 187 | // bounds[1], { 188 | // top: 40, 189 | // left: 42, 190 | // width: 140, 191 | // height: 160 192 | // }, 193 | // 'ClientUtils.getElementsBounds() is aware of the page zoom factor' 194 | // ); 195 | // }); 196 | // casper.run(function () { 197 | // test.done(); 198 | // }); 199 | //}); 200 | -------------------------------------------------------------------------------- /tests/functional/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Casper 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QUnit Example 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
      16 |
    • Running
    • 17 |
    • Cycling
    • 18 |
    • Surfing
    • 19 |
    • Jogging
    • 20 |
    21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------