/********************************************************************** * * * **********************************************************************/ ((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 }, }) // Array.prototype.flat polyfill... // // NOTE: .flat(..) is not yet supported in IE/Edge... Array.prototype.flat || (Array.prototype.flat = function(depth){ depth = typeof(depth) == typeof(123) ? depth : 1 return this.reduce(function(res, e){ return res.concat(e instanceof Array && depth > 0 ? e.flat(depth-1) : [e]) }, []) }) // Extended map... // // .emap(func) // -> array // // func has the same input signature used in .map(..) but returns an array // that will be merged into the resulting array. This frees us from the // 1:1 nature of .map(..) and adds the ability to return 0 or more items // on each iteration. // // // Example: // // double each item... // ;[1, 2, 3] // .emap(function(e){ return [e, e] }) // // -> [1, 1, 2, 2, 3, 3] // // // filter-like behaviour... // ;[1, 2, 3] // .emap(function(e){ retunr e%2 == 0 ? [] : e }) // // -> [2] // // // NOTE: if func returns a non-Array it will be placed in the resulting // array as-is... // NOTE: to return an explicit array, wrap it in an array, e.g: // ;[1, 2, 3] // .emap(function(e){ return [[e]] }) // // -> [[1], [2], [3]] Array.prototype.emap = function(func){ return this.map(func).flat() } // Compact a sparse array... // // NOTE: this will not compact in-place. Array.prototype.compact = function(){ return this.filter(function(){ return true }) } // like .length but for sparse arrays will return the element count... // XXX make this a prop... /* Array.prototype.len = function(){ //return this.compact().length return Object.keys(this).length } */ Object.defineProperty(Array.prototype, 'len', { get : function () { return Object.keys(this).length }, set : function(val){}, // NOTE: this is hear to enable running this module multiple times // without any side-effects... configurable: true, }); // Convert an array to object... // // Format: // { // : , // ... // } // // NOTE: items should be strings, other types will get converted to // strings and thus may mess things up. // NOTE: this will forget repeating items... // NOTE: normalize will slow things down... Array.prototype.toKeys = function(normalize){ return normalize ? this.reduce(function(r, e, i){ r[normalize(e)] = i return r }, {}) : this.reduce(function(r, e, i){ r[e] = i return r }, {}) } // Convert an array to a map... // // This is similar to Array.prototype.toKeys(..) but does not restrict // value type to string. // // Format: // Map([ // [, ], // ... // ]) // // NOTE: this will forget repeating items... // NOTE: normalize will slow things down... Array.prototype.toMap = function(normalize){ return normalize ? this .reduce(function(m, e, i){ m.set(normalize(e), i) return m }, new Map()) : this .reduce(function(m, e, i){ m.set(e, i) return m }, new Map()) } // 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... // NOTE: for an array containing only strings use a much faster .uniqueStrings(..) // NOTE: this may not work on IE... Array.prototype.unique = function(normalize){ return normalize ? [...new Map(this.map(function(e){ return [normalize(e), e] })).values()] : [...(new Set(this))] } // 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 } /*********************************************************************/ var selectElemText = module.selectElemText = function(elem){ var range = document.createRange() range.selectNodeContents(elem) var sel = window.getSelection() sel.removeAllRanges() sel.addRange(range) } // XXX make this global... var getCaretOffset = module.getCaretOffset = function(elem){ var s = window.getSelection() if(s.rangeCount == 0){ return -1 } var r = s.getRangeAt(0) var pre = r.cloneRange() pre.selectNodeContents(elem) pre.setEnd(r.endContainer, r.endOffset) return pre.toString().length || 0 } var selectionCollapsed = module.selectionCollapsed = function(elem){ var s = window.getSelection() if(s.rangeCount == 0){ return false } return s.getRangeAt(0).cloneRange().collapsed } /*********************************************************************/ // 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 } jQuery.fn.caretOffset = function(){ return getCaretOffset(this) } jQuery.fn.selectionCollapsed = function(){ return selectionCollapsed(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, // // // if true in multi-line mode, accept filed on Enter, while // // ctrl-Enter / meta-Enter insert a new line; otherwise // // ctrl-Enter / meta-Enter will accept the edit. // accept_on_enter: true, // // // 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 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 = Object.assign({ // defaults... activate: false, multiline: false, accept_on_enter: true, clear_on_edit: false, reset_on_commit: true, reset_on_abort: true, blur_on_commit: false, blur_on_abort: false, keep_focus_on_parent: true, clear_selection_on_commit: true, clear_selection_on_abort: true, propagate_unhandled_keys: true, reset_on_done: true, abort_keys: ['Esc'], }, options || {}) var original_text = this[0].innerText 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(in_evt){ var evt = window.event || in_evt if(!that.prop('contenteditable')){ return } evt.stopPropagation() var c = getCaretOffset(this) var collapsed = selectionCollapsed(this) var n = keyboard.code2key(evt.keyCode) // abort... if((options.abort_keys || []).indexOf(n) >= 0){ that.trigger('edit-abort', original_text) // done -- single line... } else if(n == 'Enter' && !options.multiline){ evt.preventDefault() that.trigger('edit-commit', that[0].innerText) // done -- multi-line... } else if(options.multiline && n == 'Enter' && (options.accept_on_enter ? !(evt.ctrlKey || evt.shiftKey || evt.metaKey) : (evt.ctrlKey || evt.shiftKey || evt.metaKey)) ){ evt.preventDefault() that.trigger('edit-commit', that[0].innerText) // multi-line keep keys... } else if(options.multiline && options.accept_on_enter ? (n == 'Enter' && (evt.ctrlKey || evt.shiftKey || evt.metaKey)) : n == 'Enter'){ return // multi-line arrow keys -- keep key iff not at first/last position... } else if(options.multiline && n == 'Up' && (c > 0 || !collapsed)){ return } else if(options.multiline && n == 'Down' && (c < $(this).text().length || !collapsed)){ return } else if(n == 'Up' || n == 'Down'){ evt.preventDefault() that.trigger('edit-commit', that[0].innerText) // continue handling... } else if(options.propagate_unhandled_keys){ // NOTE: jQuery can't reuse browser events, this // we need to pass a jq event/proxy here... $(this).parent().trigger(in_evt || 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 && window.getSelection().removeAllRanges() // reset original value... options.reset_on_abort && resetOriginal() options.blur_on_abort && this.blur() // restore focus on parent... options.keep_focus_on_parent && that.parents('[tabindex]').first().focus() options.reset_on_done && that.makeEditable(false) }) .on('edit-commit', events['edit-commit'] = function(evt, text){ that.trigger('edit-committing', text) options.clear_selection_on_commit && window.getSelection().removeAllRanges() // reset original value... options.reset_on_commit && resetOriginal() options.blur_on_commit && this.blur() // restore focus on parent... options.keep_focus_on_parent && that.parents('[tabindex]').first().focus() options.reset_on_done && that.makeEditable(false) }) this.data('editable-field-events', events) } return this } } /********************************************************************** * vim:set ts=4 sw=4 : */ return module })