/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ /*********************************************************************/ 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 } //--------------------------------------------------------------------- // 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(mode){ var range = document.createRange() this.each(function(){ range.selectNodeContents(this) }) var sel = window.getSelection() sel.removeAllRanges() mode === null || sel.addRange(range) return this } jQuery.fn.deselectText = function(){ this.selectText(null) 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.length == 1 ? that[0].innerText : that.toArray().map(function(e){ return e.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.length == 1 ? that[0].innerText : that.toArray().map(function(e){ return e.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.length == 1 ? that[0].innerText : that.toArray().map(function(e){ return e.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 })