diff --git a/www/js/models/sql/sourcephrase.js b/www/js/models/sql/sourcephrase.js index 40e3d983..4eff98f7 100755 --- a/www/js/models/sql/sourcephrase.js +++ b/www/js/models/sql/sourcephrase.js @@ -26,6 +26,15 @@ define(function (require) { return deferred.promise(); }, + cleanDuplicates = function() { + //DELETE FROM sourcephrase WHERE rowid NOT IN (SELECT min(rowid) FROM sourcephrase GROUP BY spid); + window.Application.db.transaction(function (tx) { + tx.executeSql("DELETE FROM sourcephrase WHERE rowid NOT IN (SELECT min(rowid) FROM sourcephrase GROUP BY spid);"); + }, function (err) { + console.log("cleanDuplicates() error: " + err.message); + }); + }, + SourcePhrase = Backbone.Model.extend({ // default values defaults: { diff --git a/www/js/views/AdaptViews.js b/www/js/views/AdaptViews.js index 703497f7..df3439ec 100644 --- a/www/js/views/AdaptViews.js +++ b/www/js/views/AdaptViews.js @@ -58,6 +58,7 @@ define(function (require) { isDrafting = true, isMergingFromKB = false, isAutoPhrase = false, + kbEvent = null, isSelectingKB = false, // is the user working with a select target text dropdown? MovingDir = 0, // -1 = backwards, 0 = not moving, 1 = forwards idx = 1, @@ -388,6 +389,7 @@ define(function (require) { // place two calls to render: // - one deferred, when we get all the source phrases for the chapter back from the DB // - one right now to say "please wait..." + this.collection.comparator = 'norder'; // sort by norder this.collection.fetch({reset: true, data: {chapterid: this.options.chapterid}}).done(this.render); this.render(); // AIM 1.6.0 - user setting to allow editing blank/empty verses @@ -553,28 +555,30 @@ define(function (require) { startIdx = 0, endIdx = content.length; // check for empty string + console.log("stripPunctuation - content: \"" + content + "\", isSource: " + isSource); if (endIdx === 0) { return result; } - if (isSource === false) { + if (isSource && isSource === true) { // starting index - while (startIdx < (content.length - 1) && punctsTarget.indexOf(content.charAt(startIdx)) > -1) { + while (startIdx < (content.length - 1) && punctsSource.indexOf(content.charAt(startIdx)) > -1) { startIdx++; } // ending index - while (endIdx > 0 && punctsTarget.indexOf(content.charAt(endIdx - 1)) > -1) { + while (endIdx > 0 && punctsSource.indexOf(content.charAt(endIdx - 1)) > -1) { endIdx--; } + } else { // starting index - while (startIdx < (content.length - 1) && punctsSource.indexOf(content.charAt(startIdx)) > -1) { + while (startIdx < (content.length - 1) && punctsTarget.indexOf(content.charAt(startIdx)) > -1) { startIdx++; } // ending index - while (endIdx > 0 && punctsSource.indexOf(content.charAt(endIdx - 1)) > -1) { + while (endIdx > 0 && punctsTarget.indexOf(content.charAt(endIdx - 1)) > -1) { endIdx--; } - } + } // sanity check for all punctuation if (endIdx <= startIdx) { return ""; @@ -789,52 +793,61 @@ define(function (require) { var keep_going = true; var FTEmpty = true; var top = 0; - console.log("moveCursor"); + var strID = $(selectedStart).attr('id'); + if (strID === undefined) { + // this might be the tt-input div if we are in a typeahead (multiple KB) input - + // if so, go up one more level to find the pile + strID = $(selectedStart.parentElement).attr('id'); + } + strID = strID.substr(strID.indexOf("-") + 1); // remove "pile-" + console.log("moveCursor: forward: " + moveForward + ", id: " + strID); event.stopPropagation(); event.preventDefault(); - // unselect the current edit field before moving + // unfocus any current selection (both the nav button that triggered the event and the selected start) $(event.currentTarget).blur(); + $(selectedStart).find(".target").blur(); + // find the model object associated with this edit field + var curSP = this.collection.findWhere({spid: strID}); if (editorMode === editorModeEnum.FREE_TRANSLATING) { // if we're doing free translation, blur the editor field so it gets saved as well $("#fteditor").blur(); } if (moveForward === false) { - // *** move backwards - if ((selectedStart.previousElementSibling !== null) && ($(selectedStart.previousElementSibling).hasClass('pile')) && (!$(selectedStart.previousElementSibling).hasClass('filter'))) { - // there is a previous sibling, and it is a non-filtered pile - next_edit = selectedStart.previousElementSibling; + // ** BACKWARDS ** + // jump to previous empty target, then select it + var idx = this.collection.indexOf(curSP) - 1; + if (idx < 0) { + // stop at the first item + curSP = null; } else { - // No previous sibling OR we've reached something we need to skip: - // - a filter - // - a header (chapter or verse) - // - a strip marker - // try skipping this item to see if we can find a "real" pile to move to - if (selectedStart.previousElementSibling !== null) { - temp_cursor = selectedStart.previousElementSibling; - // handle filtered strips and strip header elements - if (($(temp_cursor).hasClass("filter")) || ($(temp_cursor).hasClass("strip-header")) || ($(temp_cursor).hasClass("strip"))) { - // continue on to the previous item that ISN'T a strip header or filtered out of the UI - while (temp_cursor && keep_going === true) { - temp_cursor = temp_cursor.previousElementSibling; // backwards one more strip - console.log("movecursor: looking at item: " + $(temp_cursor).attr('id')); - if (temp_cursor && ($(temp_cursor).hasClass("filter") === false) && ($(temp_cursor).hasClass("pile"))) { - // found a stopping point - console.log("found stopping point: " + $(temp_cursor).attr('id')); - keep_going = false; - } - } - } - if (temp_cursor) { - next_edit = temp_cursor; - } else { - next_edit = null; - console.log("reached first pile."); + curSP = this.collection.at(idx); + } + while (curSP) { + if (curSP.get('target') === '') { + console.log("possible SP slot: "+ curSP.get('spid') + " -- " + curSP.get('source')); + // found an empty target -- is it filtered? + if ($('#pile-' + curSP.get('spid')).hasClass("filter") === false && ($('#pile-' + curSP.get('spid')).hasClass("pile"))) + { + // looks like a non-filtered pile -- break out of the while loop + break; // found the next slot } - } else { - next_edit = null; - console.log("reached first pile."); } + idx--; + if (idx < 0) { + // first pile + curSP = null; + break; + } + curSP = this.collection.at(idx); } + if (curSP) { + if ($('#pile-' + curSP.get('spid')).length !== 0) { + // everything's okay -- select the SourcePhrase + next_edit = $('#pile-' + curSP.get('spid')).get(0); + keep_going = false; + } + } // note - first node is handled at the bottom of this function + // Free Translation processing if ((editorMode === editorModeEnum.FREE_TRANSLATING) && (next_edit !== null)) { selectedEnd = lastSelectedFT = next_edit; // free translation -- lastSelectedFT is the END of the selection temp_cursor = next_edit; @@ -886,112 +899,99 @@ define(function (require) { selectedStart = next_edit; } } else { - // *** move forwards - if (editorMode === editorModeEnum.FREE_TRANSLATING) { - selectedStart = lastSelectedFT; // move from the end (not the start) of the selection + // *** FORWARDS ** + // edb 8/8/24: reworked to iterate through SP model list instead of DOM, and only stop when there's an empty target + var idx = this.collection.indexOf(curSP) + 1; + curSP = this.collection.at(idx); + while (curSP) { + if (curSP.get('target') === '') { + console.log("possible SP slot: "+ curSP.get('spid') + " -- " + curSP.get('source')); + // found an empty target -- is it filtered? + if ($('#pile-' + curSP.get('spid')).hasClass("filter") === false && ($('#pile-' + curSP.get('spid')).hasClass("pile"))) + { + // looks like a non-filtered pile -- break out of the while loop + break; // found the next slot + } + } + idx++; + curSP = this.collection.at(idx); } - if ((selectedStart.nextElementSibling !== null) && ($(selectedStart.nextElementSibling).hasClass('pile')) && (!$(selectedStart.nextElementSibling).hasClass('filter'))) { - // there is a next element (not a strip header is assumed -- strip headers will always be the first child) - next_edit = selectedStart.nextElementSibling; + if (curSP) { + if ($('#pile-' + curSP.get('spid')).length !== 0) { + // everything's okay -- select the SourcePhrase + next_edit = $('#pile-' + curSP.get('spid')).get(0); + keep_going = false; + } } else { - // no next sibling in this strip -- see if you can go to the next strip - if (selectedStart.nextElementSibling !== null) { - temp_cursor = selectedStart.nextElementSibling; - // handle filtered strips and strip header elements - if (($(temp_cursor).hasClass("filter")) || ($(temp_cursor).hasClass("strip-header")) || ($(temp_cursor).hasClass("strip"))) { - // continue on to the next strip that ISN'T filtered out of the UI - while (temp_cursor && keep_going === true) { - temp_cursor = temp_cursor.nextElementSibling; // forward one more strip - console.log("movecursor: looking at item: " + $(temp_cursor).attr('id')); - if (temp_cursor && ($(temp_cursor).hasClass("filter") === false) && ($(temp_cursor).hasClass("pile"))) { - // found a stopping point - console.log("found stopping point: " + $(temp_cursor).attr('id')); - keep_going = false; - } - } + // reached the last pile + next_edit = null; + // Check for a chapter after the current one in the current book + var nextChapter = ""; + var book = window.Application.BookList.where({bookid: chapter.get('bookid')}); + var chaps = book[0].get('chapters'); + if (chaps.length > 1) { + if ((chaps.indexOf(chapter.get('chapterid')) !== -1) && + (chaps.indexOf(chapter.get('chapterid')) < (chaps.length - 1))) { + // There is a chapter after this one + nextChapter = chaps[chaps.indexOf(chapter.get('chapterid')) + 1]; } - if (temp_cursor) { - // found a strip that doesn't have a filter -- select the first pile - // (note that this will also skip the strip header div, which is what we want) - next_edit = temp_cursor; - } else { - next_edit = null; - console.log("reached last pile."); - } - } else { - // no more piles - next_edit = null; - console.log("reached last pile."); } - // if we reached the last pile, check to see if there's another chapter to adapt - if (next_edit === null) { - // Check for a chapter after the current one in the current book - var nextChapter = ""; - var book = window.Application.BookList.where({bookid: chapter.get('bookid')}); - var chaps = book[0].get('chapters'); - if (chaps.length > 1) { - if ((chaps.indexOf(chapter.get('chapterid')) !== -1) && - (chaps.indexOf(chapter.get('chapterid')) < (chaps.length - 1))) { - // There is a chapter after this one - nextChapter = chaps[chaps.indexOf(chapter.get('chapterid')) + 1]; - } - } - - // If there is a next chapter, let the user continue or exit; - // if there isn't one, just allow them to exit - if (navigator.notification) { - // on mobile device - navigator.notification.beep(1); - if (nextChapter.length > 0) { - navigator.notification.confirm( - i18next.t('view.dscAdaptContinue', {chapter: chapter.get('name')}), - function (buttonIndex) { - if (buttonIndex === 1) { - // Next chapter - // update the URL, but replace the history (so we go back to the welcome screen) - window.Application.router.navigate("adapt/" + nextChapter, {trigger: true, replace: true}); - - } else { - // exit - // save the model - chapter.trigger('change'); - // head back to the home page - window.Application.home(); - } - }, - i18next.t('view.ttlMain'), - [i18next.t('view.lblNext'), i18next.t('view.lblFinish')] - ); - } else { - // no option to continue, just one to exit - navigator.notification.alert( - i18next.t('view.dscAdaptComplete', {chapter: chapter.get('name')}), - function () { + // If there is a next chapter, let the user continue or exit; + // if there isn't one, just allow them to exit + if (navigator.notification) { + // on mobile device + navigator.notification.beep(1); + if (nextChapter.length > 0) { + navigator.notification.confirm( + i18next.t('view.dscAdaptContinue', {chapter: chapter.get('name')}), + function (buttonIndex) { + if (buttonIndex === 1) { + // Next chapter + // update the URL, but replace the history (so we go back to the welcome screen) + window.Application.router.navigate("adapt/" + nextChapter, {trigger: true, replace: true}); + } else { // exit // save the model chapter.trigger('change'); // head back to the home page window.Application.home(); } - ); - } + }, + i18next.t('view.ttlMain'), + [i18next.t('view.lblNext'), i18next.t('view.lblFinish')] + ); } else { - // in browser - if (nextChapter > 0) { - if (confirm(i18next.t('view.dscAdaptContinue', {chapter: chapter.get('name')}))) { - // update the URL, but replace the history (so we go back to the welcome screen) - window.Application.router.navigate("adapt/" + nextChapter, {trigger: true, replace: true}); - } else { + // no option to continue, just one to exit + navigator.notification.alert( + i18next.t('view.dscAdaptComplete', {chapter: chapter.get('name')}), + function () { + // exit + // save the model + chapter.trigger('change'); + // head back to the home page window.Application.home(); } + ); + } + } else { + // in browser + if (nextChapter > 0) { + if (confirm(i18next.t('view.dscAdaptContinue', {chapter: chapter.get('name')}))) { + // update the URL, but replace the history (so we go back to the welcome screen) + window.Application.router.navigate("adapt/" + nextChapter, {trigger: true, replace: true}); } else { - alert(i18next.t('view.dscAdaptComplete', {chapter: chapter.get('name')})); window.Application.home(); } + } else { + alert(i18next.t('view.dscAdaptComplete', {chapter: chapter.get('name')})); + window.Application.home(); } - } } + // Free Translation processing + if (editorMode === editorModeEnum.FREE_TRANSLATING) { + selectedStart = lastSelectedFT; // move from the end (not the start) of the selection + } if ((editorMode === editorModeEnum.FREE_TRANSLATING) && (next_edit !== null)) { // in FT mode, we need to also find the end of the selection selectedStart = selectedEnd = lastSelectedFT = temp_cursor = next_edit; // initial value @@ -1038,7 +1038,8 @@ define(function (require) { // Set selectedEnd and lastSelectedFT to the end of the selection selectedEnd = lastSelectedFT = next_edit; } - } + } + // done moving the cursor -- now select it if possible if (next_edit) { // simulate a click on the next edit field console.log("next edit: " + next_edit.id); @@ -1133,7 +1134,6 @@ define(function (require) { "touchmove .pile": "selectingPilesMove", "mouseup .pile": "selectingPilesEnd", "touchend .pile": "selectingPilesEnd", - "keydown .pile": "mergeAndReplace", "doubletap .pile": "onDblTapPile", "mouseup .filter": "showFilter", "touchend .filter": "showFilter", @@ -2182,6 +2182,30 @@ define(function (require) { $("#PrevSP").prop('disabled', false); // enable toolbar button $("#NextSP").prop('disabled', false); // enable toolbar button isEditing = true; + // Is this an auto-merge? + if (isAutoPhrase === true) { + isAutoPhrase = false; // clear out flag + // clear out the existing text (if any) + $(event.currentTarget).html(""); + isDirty = true; + // select any text in the edit field + if (document.body.createTextRange) { + range = document.body.createTextRange(); + range.moveToElementText($(event.currentTarget)); + range.select(); + } else if (window.getSelection) { + selection = window.getSelection(); + selection.removeAllRanges(); + range = document.createRange(); + range.selectNodeContents($(event.currentTarget)[0]); + selection.addRange(range); + } + // send our pocketed keydown character event to the target field + var e = jQuery.Event( "keydown", { keyCode: kbEvent.keyCode, key : kbEvent.key, code: kbEvent.code, shiftKey: kbEvent.shiftKey } ); + $(event.currentTarget).trigger(e); + kbEvent = null; // clear out event + return; // don't continue processing here + } // Is the target field empty? if ($(event.currentTarget).text().trim().length === 0) { // target is empty -- attempt to populate it @@ -2295,7 +2319,7 @@ define(function (require) { range.selectNodeContents($(event.currentTarget)[0]); selection.addRange(range); } - if (navigator.notification && Keyboard) { + if (Keyboard && typeof Keyboard.show === 'function') { Keyboard.show(); } // it's possible that we went offscreen while looking for the next available slot to adapt. @@ -2343,7 +2367,7 @@ define(function (require) { $(event.currentTarget).html(""); } else { // copy the source text - $(event.currentTarget).html(this.stripPunctuation(sourceText), true); + $(event.currentTarget).html(this.stripPunctuation(sourceText, true)); } MovingDir = 0; // stop here clearKBInput = true; @@ -2659,7 +2683,7 @@ define(function (require) { $(event.currentTarget).html(""); } else { // copy the source text - $(event.currentTarget).html(this.stripPunctuation(sourceText), true); + $(event.currentTarget).html(this.stripPunctuation(sourceText, true)); } MovingDir = 0; // stop here clearKBInput = true; @@ -2773,18 +2797,6 @@ define(function (require) { // console.log("selectedGloss exit / isDirty = " + isDirty + ", origText = " + origText); }, - // keydown event handler for the _pile_: this is the "select and type" - // shortcut / use case (#109), and requires a bluetooth keyboard to operate - // (i.e., there will not be a soft keyboard displayed if the pile has focus). - // Our goal here is to merge the selection (if there is more than one pile), and replace the resulting - // target texts with whatever the user just typed in. - mergeAndReplace: function (event) { - console.log("mergeAndReplace(): entry"); - // don't bubble this event up to the parent - // event.stopPropagation(); - // event.preventDefault(); - }, - // keydown event handler for the target field editAdaptation: function (event) { var strID = null, @@ -2815,11 +2827,9 @@ define(function (require) { selectedEnd = selectedStart; } if (event.shiftKey) { - MovingDir = -1; - this.moveCursor(event, false); // shift tab/enter -- move backwards + $("#PrevSP").mouseup(); // trigger prev SP button event } else { - MovingDir = 1; - this.moveCursor(event, true); // normal tab/enter -- move forwards + $("#NextSP").mouseup(); // trigger next SP button event } } else { // any other key - set the dirty bit @@ -3008,8 +3018,8 @@ define(function (require) { console.log("Dirty bit set. Saving KB value: " + trimmedValue); // something has changed -- update the KB saveInKB(this.stripPunctuation(this.autoRemoveCaps(model.get('source'), true), true), - Underscore.escape(this.stripPunctuation(this.autoRemoveCaps(trimmedValue, false)).trim(), false), - Underscore.escape(this.stripPunctuation(this.autoRemoveCaps(model.get('target'), false)).trim(), false), + Underscore.escape(this.stripPunctuation(this.autoRemoveCaps(trimmedValue, false), false).trim(), false), + Underscore.escape(this.stripPunctuation(this.autoRemoveCaps(model.get('target'), false), false).trim(), false), project.get('projectid'), 0); } // add any punctuation back to the target field @@ -3150,8 +3160,8 @@ define(function (require) { console.log("Dirty bit set. Saving KB value: " + trimmedValue); // something has changed -- update the KB saveInKB(this.stripPunctuation(this.autoRemoveCaps(model.get('source'), true), true), - Underscore.escape(this.stripPunctuation(this.autoRemoveCaps(trimmedValue, false)).trim(), false), - Underscore.escape(this.stripPunctuation(this.autoRemoveCaps(model.get('gloss'), false)).trim(), false), + Underscore.escape(this.stripPunctuation(this.autoRemoveCaps(trimmedValue, false), false).trim(), false), + Underscore.escape(this.stripPunctuation(this.autoRemoveCaps(model.get('gloss'), false), false).trim(), false), project.get('projectid'), 1); } // add any punctuation back to the target field @@ -3698,7 +3708,7 @@ define(function (require) { if (isMergingFromKB === false) { // NOT merging from the KB (i.e., an automatic merge); so the user has merged this phrase -- // is there something in the KB that matches this phrase? - tu = this.findInKB(this.stripPunctuation(this.autoRemoveCaps(phraseSource, true)), 0); + tu = this.findInKB(this.stripPunctuation(this.autoRemoveCaps(phraseSource, true), true), 0); if (tu !== null) { // found at least one match -- populate the target with the first match refstrings = tu.get('refstring'); @@ -3714,22 +3724,7 @@ define(function (require) { phraseHtml += this.stripPunctuation(this.autoAddCaps(phObj, refstrings[0].target), false); isDirty = true; } - } else { - // nothing in the KB -- leave the target field blank. We will trigger the target selection below; the selection even handler will fill in - // the source from this newly-merged phrase. - // TODO: autophrase / select and type mechanism... - // console.log("isAutoPhrase: " + isAutoPhrase); - // if (isAutoPhrase === false) { - // // if there's something already in the target, use it instead - // phraseHtml += (phraseTarget.trim().length > 0) ? phraseTarget : phraseSource; - // isDirty = false; // don't save (original sourcephrase is now gone) - // } else { - // // autophrase -- add the target for the selected start ONLY - // phraseHtml += $(selectedStart).find(".target").html(); - // isDirty = true; // save - // } - // isAutoPhrase = false; // clear the autophrase flag - } + } } phraseHtml += PhraseLine4; console.log("phrase: " + phraseHtml); @@ -3828,9 +3823,6 @@ define(function (require) { removeFromKB(this.stripPunctuation(this.autoRemoveCaps(selectedObj.get('source'), true), true), this.stripPunctuation(this.autoRemoveCaps(selectedObj.get('target'), false).trim(), false), project.get('projectid'), 0); - - -// removeFromKB(this.autoRemoveCaps(selectedObj.get("source")), selectedObj.get("target"), project.get('projectid'), 0); // remove from KB this.collection.remove(selectedObj); // remove from collection selectedObj.destroy(); // delete the object from the database $(selectedStart).remove(); @@ -4068,6 +4060,7 @@ define(function (require) { kblist = new kbModels.TargetUnitCollection(); document.addEventListener("pause", this.onPause, false); document.addEventListener("resume", this.onResume, false); + window.addEventListener("keydown", this.checkAutoMerge, false); USFMMarkers = new usfm.MarkerCollection(); USFMMarkers.fetch({reset: true, data: {name: ""}}); // return all results }, @@ -4095,6 +4088,32 @@ define(function (require) { // refresh the view Backbone.history.loadUrl(Backbone.history.fragment); }, + checkAutoMerge: function () { + console.log("checkAutoMerge / keydown event"); + if (!(event.target.id === "main")) { + // not our event (we want the window event)-- exit + return; + } + if (editorMode !== editorModeEnum.ADAPTING) { + // we only care if the user is trying to adapt + return; + } + if ((selectedStart !== null) && (selectedEnd !== null) && (selectedStart !== selectedEnd)) { + // user has selected more than one pile, then pressed a key - + // if it's a "normal" key, pocket the event and merge the piles (we'll handle the keydown event + // in the selectedAdaptation() handler) + if ((event.keyCode === 27) || (event.keyCode === 9) || (event.keyCode === 13)) { + // ignore special chars (Esc, enter, return) + } else { + // any other key - keep the keypress event and trigger a merge + isAutoPhrase = true; + kbEvent = event; + // this.togglePhrase(); + // attempt to jump back into proper "this" context (we're currently under the window object) + window.Application.main.currentView.listView.togglePhrase(event); + } + } + }, onShow: function () { console.log("ChapterView::onShow"); project = this.project;