diff --git a/ui (gen4)/lib/util-dom.js b/ui (gen4)/lib/util-dom.js new file mode 100755 index 00000000..0ec3d39e --- /dev/null +++ b/ui (gen4)/lib/util-dom.js @@ -0,0 +1,383 @@ +/********************************************************************** +* +* +* +**********************************************************************/ +((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(){ + 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 }) diff --git a/ui (gen4)/lib/util.js b/ui (gen4)/lib/util.js index ad2ace18..ea125d37 100755 --- a/ui (gen4)/lib/util.js +++ b/ui (gen4)/lib/util.js @@ -391,375 +391,6 @@ function(path){ -/*********************************************************************/ -// HTML/DOM/jQuery... - -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(){ - 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 }) diff --git a/ui (gen4)/ui.js b/ui (gen4)/ui.js index a8a060c0..f9b7d32d 100755 --- a/ui (gen4)/ui.js +++ b/ui (gen4)/ui.js @@ -45,6 +45,7 @@ if(typeof(process) != 'undefined'){ function(require){ var module={} // makes module AMD/node compatible... /*********************************************************************/ +var utildom = require('lib/util-dom') var viewer = require('imagegrid/viewer')