/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ /*********************************************************************/ String.prototype.capitalize = function(){ return this == '' ? this : this[0].toUpperCase() + this.slice(1) } // XXX not sure if this has to be a utility or a method... Object.get = function(obj, name, dfl){ var val = obj[name] if(val === undefined && dfl != null){ return dfl } return val } Object.defineProperty(Object.prototype, 'run', { enumerable: false, value: function(func){ var res = func ? func.call(this) : undefined return res === undefined ? this : res }, }) // Compact a sparse array... // // NOTE: this will not compact in-place. Array.prototype.compact = function(){ return this.filter(function(){ return true }) } /* Array.prototype.compact = function(){ var res = [] for(var i in res){ res.push(this[i]) } return res } */ // return an array with duplicate elements removed... // // NOTE: we are not using an Object as an index here as an Array can // contain any type of item while Object keys can only be strings... Array.prototype.unique = function(normalize){ if(normalize){ var cache = this.map(function(e){ return normalize(e) }) return this.filter(function(e, i, a){ return cache.indexOf(cache[i]) == i }) } else { return this.filter(function(e, i, a){ return a.indexOf(e) == i }) } } // Compare two arrays... // Array.prototype.cmp = function(other){ if(this === other){ return true } if(this.length != other.length){ return false } for(var i=0; i])/g, '\\$1') } // XXX do we need to quote anything else??? var path2url = module.path2url = function(path){ // test if we have a schema, and if yes return as-is... if(/^(data|http|https|file|[\w-]*):[\\\/]{2}/.test(path)){ return path } // skip encoding windows drives... path = path .split(/[\\\/]/g) drive = path[0].endsWith(':') ? path.shift() + '/' : '' return drive + (path // XXX these are too aggressive... //.map(encodeURI) //.map(encodeURIComponent) .join('/') // NOTE: keep '%' the first... .replace(/%/g, '%25') .replace(/#/g, '%23') .replace(/&/g, '%26')) } // NOTE: we are not using node's path module as we need this to work in // all contexts, not only node... (???) var normalizePath = module.normalizePath = function(path){ return typeof(path) == typeof('str') ? path // normalize the slashes... .replace(/\\/g, '/') // remove duplicate '/' .replace(/(\/)\1+/g, '/') // remove trailing '/' .replace(/\/+$/, '') // take care of . .replace(/\/\.\//g, '/') .replace(/\/\.$/, '') // take care of .. .replace(/\/[^\/]+\/\.\.\//g, '/') .replace(/\/[^\/]+\/\.\.$/, '') : path } /*********************************************************************/ module.selectElemText = function(elem){ var range = document.createRange() range.selectNodeContents(elem) var sel = window.getSelection() sel.removeAllRanges() sel.addRange(range) } /*********************************************************************/ // NOTE: repatching a date should not lead to any side effects as this // does not add any state... var patchDate = module.patchDate = function(date){ date = date || Date date.prototype.toShortDate = function(){ var y = this.getFullYear() var M = this.getMonth()+1 M = M < 10 ? '0'+M : M var D = this.getDate() D = D < 10 ? '0'+D : D var H = this.getHours() H = H < 10 ? '0'+H : H var m = this.getMinutes() m = m < 10 ? '0'+m : m var s = this.getSeconds() s = s < 10 ? '0'+s : s return ''+y+'-'+M+'-'+D+' '+H+':'+m+':'+s } date.prototype.getTimeStamp = function(no_seconds){ var y = this.getFullYear() var M = this.getMonth()+1 M = M < 10 ? '0'+M : M var D = this.getDate() D = D < 10 ? '0'+D : D var H = this.getHours() H = H < 10 ? '0'+H : H var m = this.getMinutes() m = m < 10 ? '0'+m : m var s = this.getSeconds() s = s < 10 ? '0'+s : s return ''+y+M+D+H+m+s } date.prototype.setTimeStamp = function(ts){ ts = ts.replace(/[^0-9]*/g, '') this.setFullYear(ts.slice(0, 4)) this.setMonth(ts.slice(4, 6)*1-1) this.setDate(ts.slice(6, 8)) this.setHours(ts.slice(8, 10)) this.setMinutes(ts.slice(10, 12)) this.setSeconds(ts.slice(12, 14)) return this } date.timeStamp = function(){ return (new this()).getTimeStamp() } date.fromTimeStamp = function(ts){ return (new this()).setTimeStamp(ts) } // convert string time period to milliseconds... date.str2ms = function(str, dfl){ dfl = dfl || 'ms' if(typeof(str) == typeof(123)){ var val = str str = dfl } else { var val = parseFloat(str) str = str.trim() // check if a unit is given... str = str == val ? dfl : str } var c = /(m(illi)?(-)?s(ec(ond(s)?)?)?)$/i.test(str) ? 1 : /s(ec(ond(s)?)?)?$/i.test(str) ? 1000 : /m(in(ute(s)?)?)?$/i.test(str) ? 1000*60 : /h(our(s)?)?$/i.test(str) ? 1000*60*60 : /d(ay(s)?)?$/i.test(str) ? 1000*60*60*24 : null return c ? val * c : NaN } return date } // patch the root date... patchDate() /*********************************************************************/ // XXX experiment if(typeof(jQuery) != typeof(undefined)){ jQuery.fn._drag = function(){ var dragging = false var s, px, py var elem = $(this) .on('mousedown touchstart', function(evt){ dragging = true px = evt.clientX px = evt.clientY s = elem.rscale() }) .on('mousemove touchmove', function(evt){ if(!dragging){ return } var x = evt.clientX var dx = px - x px = x var y = evt.clientY var dy = py - y py = y elem .velocity('stop') .velocity({ translateX: '-=' + (dx / s), translateY: '-=' + (dy / s), }, 0) }) .on('mouseup touchend', function(evt){ dragging = false elem.velocity('stop') }) } jQuery.fn.selectText = function(){ var range = document.createRange() this.each(function(){ range.selectNodeContents(this) }) var sel = window.getSelection() sel.removeAllRanges() sel.addRange(range) return this } var keyboard = require('lib/keyboard') // Make element editable... // // Options format: // { // // activate (focus) element... // // // // NOTE: this will also select the element text... // activate: false, // // // set multi line edit mode... // multiline: false, // // // clear element value on edit... // clear_on_edit: false, // // // reset value on commit/abort... // // XXX revise default... // reset_on_commit: true, // reset_on_abort: true, // // // blur element on commit/abort... // blur_on_commit: false, // blur_on_abort: false, // // // restore focus before disabling the editor... // keep_focus_on_parent: true, // // // clear selection on commit/abort... // clear_selection_on_commit: true, // clear_selection_on_abort: true, // // // If false unhandled key events will not be propagated to // // parents... // propagate_unhandled_keys: true, // // // If false the element editable state will not be reset to // // the original when edit is done... // reset_on_done: true, // // // Keys that will abort the edit... // abort_keys: [ // 'Esc', // ], // } // // This listens to these events triggerable by user: // 'edit-commit' - will commit changes, this is passed the // new text just edited. // 'edit-abort' - will reset field, this is passed the // original text before the edit. // // These events get passed the relevant text, but the element is // likely to be already reset to a different state, to get the // element before any state change is started use one of the // following variants: // 'edit-committing' - triggered within 'edit-commit' but before // anything is changed, gets passed the final // text (same as 'edit-commit') // 'edit-aborting' - triggered within 'edit-abort' but before // anything is changed, gets passed the // original text value (same as 'edit-abort') // // This will try and preserve element content DOM when resetting. // // // NOTE: removing tabindex will reset focus, so this will attempt to // focus the first [tabindex] element up the tree... // // XXX add option to select the element on start or just focus it... // .activate: 'select' | true | false // XXX multiline fields need to retain original function of arrows // until we are at last line, then pass it through... // XXX should we just use form elements??? // ...it's a trade-off, here we add editing functionality and fight // a bit the original function, in an input we'll need to fight part // of the editing functionality and add our own navigation... // XXX move this to a more generic spot... jQuery.fn.makeEditable = function(options){ var that = this if(options == false){ this .removeAttr('contenteditable') .removeAttr('tabindex') .removeClass('editable-field') var events = this.data('editable-field-events') for(var e in events){ this.off(e, events[e]) } this.removeData('editable-field-events') return this } options = options || {} var original_text = this.text() var original_dom = document.createDocumentFragment() this[0].childNodes .forEach(function(node){ original_dom.appendChild(node.cloneNode(true)) }) var resetOriginal = function(){ //that.text(original_text) that[0].innerHTML = '' that[0].appendChild(original_dom.cloneNode(true)) } this.prop('contenteditable', true) options.activate && options.clear_on_edit // XXX this for some reason breaks on click... && this.text('') // NOTE: this will also focus the element... options.activate && this.selectText() // do not setup handlers more than once... if(!this.hasClass('editable-field')){ var events = {} this // make the element focusable and selectable... .attr('tabindex', '0') .addClass('editable-field') .keydown(events.keydown = function(evt){ if(!that.prop('contenteditable')){ return } event.stopPropagation() var n = keyboard.code2key(event.keyCode) // abort... if((options.abort_keys || ['Esc']).indexOf(n) >= 0){ that.trigger('edit-abort', original_text) // done -- single line... } else if(n == 'Enter' && !options.multiline){ event.preventDefault() that.trigger('edit-commit', that.text()) // done -- multi-line... } else if(n == 'Enter' && (event.ctrlKey || event.metaKey) && options.multiline){ event.preventDefault() that.trigger('edit-commit', that.text()) // continue handling... } else if(options.propagate_unhandled_keys !== false){ $(this).parent().trigger(evt) } }) .blur(events.blur = function(){ window.getSelection().removeAllRanges() }) .on('focus click', events['focus click'] = function(evt){ evt.stopPropagation() options.clear_on_edit && $(this) .text('') .selectText() }) // user triggerable events... .on('edit-abort', events['edit-abort'] = function(evt, text){ that.trigger('edit-aborting', text) options.clear_selection_on_abort !== false && window.getSelection().removeAllRanges() // reset original value... options.reset_on_abort !== false && resetOriginal() options.blur_on_abort !== false && this.blur() // restore focus on parent... options.keep_focus_on_parent !== false && that.parents('[tabindex]').first().focus() options.reset_on_done !== false && that.makeEditable(false) }) .on('edit-commit', events['edit-commit'] = function(evt, text){ that.trigger('edit-committing', text) options.clear_selection_on_commit !== false && window.getSelection().removeAllRanges() // reset original value... options.reset_on_commit !== false && resetOriginal() options.blur_on_commit !== false && this.blur() // restore focus on parent... options.keep_focus_on_parent !== false && that.parents('[tabindex]').first().focus() options.reset_on_done !== false && that.makeEditable(false) }) this.data('editable-field-events', events) } return this } } /********************************************************************** * vim:set ts=4 sw=4 : */ return module })