diff --git a/src/content-handlers/iiif/extensions/uv-openseadragon-extension/config/config.json b/src/content-handlers/iiif/extensions/uv-openseadragon-extension/config/config.json index 2f02fb281..2d5191fa0 100644 --- a/src/content-handlers/iiif/extensions/uv-openseadragon-extension/config/config.json +++ b/src/content-handlers/iiif/extensions/uv-openseadragon-extension/config/config.json @@ -288,13 +288,13 @@ "autocompleteAllowWords": false, "galleryButtonEnabled": true, "imageSelectionBoxEnabled": false, - "pageModeEnabled": false, + "pageModeEnabled": true, "pagingToggleEnabled": true, "centerOptionsEnabled": true, "localeToggleEnabled": false, "settingsButtonEnabled": true, "helpEnabled": true, - "modeOptionsEnabled": true + "modeOptionsEnabled": false }, "content": { "close": "$close", diff --git a/src/content-handlers/iiif/modules/uv-pagingheaderpanel-module/PagingHeaderPanel.ts b/src/content-handlers/iiif/modules/uv-pagingheaderpanel-module/PagingHeaderPanel.ts index 43237080e..7dfad8071 100644 --- a/src/content-handlers/iiif/modules/uv-pagingheaderpanel-module/PagingHeaderPanel.ts +++ b/src/content-handlers/iiif/modules/uv-pagingheaderpanel-module/PagingHeaderPanel.ts @@ -1,5 +1,5 @@ const $ = require("jquery"); -import { AutoComplete } from "../uv-shared-module/AutoComplete"; +import { PagingAutoComplete } from "../uv-shared-module/PagingAutoComplete"; import { IIIFEvents } from "../../IIIFEvents"; import { OpenSeadragonExtensionEvents } from "../../extensions/uv-openseadragon-extension/Events"; import { HeaderPanel } from "../uv-shared-module/HeaderPanel"; @@ -135,20 +135,25 @@ export class PagingHeaderPanel extends HeaderPanel< ); this.$search.append(this.$autoCompleteBox); - new AutoComplete( + new PagingAutoComplete( + //element this.$autoCompleteBox, + + //autoCompleteFunc (term: string, cb: (results: string[]) => void) => { const results: string[] = []; const canvases: Canvas[] = this.extension.helper.getCanvases(); + // if in page mode, get canvases by label. if (this.isPageModeEnabled()) { + for (let i = 0; i < canvases.length; i++) { const canvas: Canvas = canvases[i]; const label: string | null = LanguageMap.getValue( canvas.getLabel() ); - if (label && label.startsWith(term)) { + if (label && label.includes(term)) { results.push(label); } } @@ -163,14 +168,27 @@ export class PagingHeaderPanel extends HeaderPanel< } cb(results); }, + + //parseResultsFunc (results: any) => { return results; }, + + //onSelect (terms: string) => { this.search(terms); }, - 300, + + //delay 0, + + // minChars + 0, + + //positionAbove + false, + + //allowWords Bools.getBool(this.options.autocompleteAllowWords, false) ); } else if (Bools.getBool(this.options.imageSelectionBoxEnabled, true)) { @@ -418,6 +436,13 @@ export class PagingHeaderPanel extends HeaderPanel< if (this.options.modeOptionsEnabled === false) { this.$modeOptions.hide(); this.$centerOptions.addClass("modeOptionsDisabled"); + + //JM also hide other parts of the panel otherwise viewable in imageMode + this.$searchButton.hide(); + this.$total.hide(); + + //JM and add class to autocomplate input to re-style + this.$autoCompleteBox.addClass("pageMode"); } // Search is shown as default @@ -456,6 +481,16 @@ export class PagingHeaderPanel extends HeaderPanel< if (!Bools.getBool(this.options.pagingToggleEnabled, true)) { this.$pagingToggleButtons.hide(); } + + $(document).on("click", (e) => { + if ( + !this.$autoCompleteBox.is($(e.target)[0]) && + this.$autoCompleteBox.has($(e.target)[0]).length === 0 + ) { + this.setSearchFieldValue(this.extension.helper.canvasIndex); + } + }) + } openGallery(): void { @@ -753,5 +788,8 @@ export class PagingHeaderPanel extends HeaderPanel< if (this.pagingToggleIsVisible()) this.$pagingToggleButtons.show(); if (this.galleryIsVisible()) this.$galleryButton.show(); } - } + }; + + } + diff --git a/src/content-handlers/iiif/modules/uv-pagingheaderpanel-module/css/styles.less b/src/content-handlers/iiif/modules/uv-pagingheaderpanel-module/css/styles.less index 8c11982d8..c4584bad2 100644 --- a/src/content-handlers/iiif/modules/uv-pagingheaderpanel-module/css/styles.less +++ b/src/content-handlers/iiif/modules/uv-pagingheaderpanel-module/css/styles.less @@ -53,7 +53,7 @@ .search { float: left; - margin: 6px 0 0 5px; + margin: 6px 5px 0 5px; width: 113px; .searchText { @@ -102,13 +102,22 @@ padding: 0; margin-top: 6px; color: black; + + &.pageMode { + width: 113px; + text-align: center; + height: 25px; + margin-top: 1px; + } + } .autocomplete { position: absolute; background: #fff; - width: 60px; - border: 2px solid @brand-primary-lighter; + width: 113px; + // border: 2px solid @brand-primary-lighter; + border: 0px; list-style-type: none; -webkit-margin-before: 0px; -webkit-margin-after: 0px; @@ -123,7 +132,8 @@ z-index: 1000; .result { - padding: 4px; + padding: 0 0 0 5px; + height: 25px; width: 270px; overflow: hidden; @@ -137,6 +147,26 @@ &.selected { background: @gray-lighter; } + + &:hover { + background: @gray-lighter; + } + + a { + display: block; + height: 25px; + line-height: 2.2em; + border: 0; + + &:hover { + text-decoration: none; + } + + &:active, &:focus { + outline: 0; + border: none; + } + } } } } diff --git a/src/content-handlers/iiif/modules/uv-shared-module/AutoComplete.ts b/src/content-handlers/iiif/modules/uv-shared-module/AutoComplete.ts index 02b5e1720..d226fcd76 100644 --- a/src/content-handlers/iiif/modules/uv-shared-module/AutoComplete.ts +++ b/src/content-handlers/iiif/modules/uv-shared-module/AutoComplete.ts @@ -124,7 +124,7 @@ export class AutoComplete { // if there are more than x chars // update the autocomplete list. - if (val && val.length > that._minChars && that._searchForWords(val)) { + if (val && val.length >= that._minChars && that._searchForWords(val)) { that._search(val); } else { // otherwise, hide the autocomplete list. @@ -166,7 +166,7 @@ export class AutoComplete { } private _getTerms(): string { - return this._$element.val().trim(); + return this._$element.val().trim(); } private _setSelectedResultIndex(direction: number): void { diff --git a/src/content-handlers/iiif/modules/uv-shared-module/NewAutoComplete.ts b/src/content-handlers/iiif/modules/uv-shared-module/NewAutoComplete.ts new file mode 100644 index 000000000..71d85bbc4 --- /dev/null +++ b/src/content-handlers/iiif/modules/uv-shared-module/NewAutoComplete.ts @@ -0,0 +1,296 @@ +const $ = require("jquery"); +import * as KeyCodes from "@edsilv/key-codes"; +import { Keyboard } from "@edsilv/utils"; +import { isVisible } from "../../../../Utils"; +export class AutoComplete { + private _results: any; + private _selectedResultIndex: number; + private _$element: JQuery; + private _autoCompleteFunc: ( + terms: string, + cb: (results: string[]) => void + ) => void; + private _delay: number; + private _minChars: number; + private _onSelect: (terms: string) => void; + private _parseResultsFunc: (results: string[]) => string[]; + private _positionAbove: boolean; + private _allowWords: boolean; + + private _$searchResultsList: JQuery; + private _$searchResultTemplate: JQuery; + + constructor( + element: JQuery, + autoCompleteFunc: (terms: string, cb: (results: string[]) => void) => void, + parseResultsFunc: (results: any) => string[], + onSelect: (terms: string) => void, + delay: number = 300, + minChars: number = 2, + positionAbove: boolean = false, + allowWords: boolean = false + ) { + this._$element = element; + this._autoCompleteFunc = autoCompleteFunc; + this._delay = delay; + this._minChars = minChars; + this._onSelect = onSelect; + this._parseResultsFunc = parseResultsFunc; + this._positionAbove = positionAbove; + this._allowWords = allowWords; + + // create ui. + this._$searchResultsList = $(''); + + if (this._positionAbove) { + this._$element.parent().prepend(this._$searchResultsList); + } else { + this._$element.parent().append(this._$searchResultsList); + } + + this._$searchResultTemplate = $( + '
  • ' + ); + + // init ui. + + // callback after set period. + const typewatch = (function() { + let timer: number = 0; + return function(cb: Function, ms: number) { + clearTimeout(timer); + timer = setTimeout(cb, ms); + }; + })(); + + const that = this; + + this._$element.on("keydown", function(e: JQueryEventObject) { + const originalEvent: KeyboardEvent = e.originalEvent; + //that._lastKeyDownWasNavigation = that._isNavigationKeyDown(originalEvent); + const charCode: number = Keyboard.getCharCode(originalEvent); + let cancelEvent: boolean = false; + + if (charCode === KeyCodes.KeyDown.LeftArrow) { + cancelEvent = true; + } else if (charCode === KeyCodes.KeyDown.RightArrow) { + cancelEvent = true; + } + + if (cancelEvent) { + originalEvent.cancelBubble = true; + if (originalEvent.stopPropagation) originalEvent.stopPropagation(); + } + }); + + // this._$element.on("blur", () => { + // that._clearResults(); + // that._hideResults(); + // }); + + // auto complete + this._$element.on("keyup", function(e) { + // if pressing enter without a list item selected + if ( + !that._getSelectedListItem().length && + e.keyCode === KeyCodes.KeyDown.Enter + ) { + // enter + that._onSelect(that._getTerms()); + return; + } + + if (e.keyCode === KeyCodes.KeyDown.Tab) { + return; + } + + // If there are search results + if (isVisible(that._$searchResultsList) && that._results.length) { + if (e.keyCode === KeyCodes.KeyDown.Enter) { + that._searchForItem(that._getSelectedListItem()); + } else if (e.keyCode === KeyCodes.KeyDown.DownArrow) { + that._setSelectedResultIndex(1); + return; + } else if (e.keyCode === KeyCodes.KeyDown.UpArrow) { + that._setSelectedResultIndex(-1); + return; + } + } + + if (e.keyCode !== KeyCodes.KeyDown.Enter) { + // after a delay, show autocomplete list. + typewatch(() => { + const val = that._getTerms(); + + // if there are more than x chars + // update the autocomplete list. + if (val && val.length > that._minChars && that._searchForWords(val)) { + that._search(val); + } else { + // otherwise, hide the autocomplete list. + that._clearResults(); + that._hideResults(); + } + }, that._delay); + } + }); + + // hide results if clicked outside. + $(document).on("mouseup", (e) => { + if (this._$searchResultsList.parent().has($(e.target)[0]).length === 0) { + this._clearResults(); + this._hideResults(); + } + }); + + // hide results if focus moves on. + $(document).on("focusin", (e) => { + if ( + this._$searchResultsList.has($(e.target)[0]).length === 0 && + !this._$element.is($(e.target)[0]) + ) { + this._clearResults(); + this._hideResults(); + } + }); + + this._hideResults(); + } + + private _searchForWords(search: string): boolean { + if (this._allowWords || !search.includes(" ")) { + return true; + } else { + return false; + } + } + + private _getTerms(): string { + if (this._$element.val()) { + return this._$element.val().trim(); + } else { + return "" + } + + } + + private _setSelectedResultIndex(direction: number): void { + let nextIndex: number; + + if (direction === 1) { + nextIndex = this._selectedResultIndex + 1; + } else { + nextIndex = this._selectedResultIndex - 1; + } + + const $items: JQuery = this._$searchResultsList.find("li"); + + if (nextIndex < 0) { + nextIndex = $items.length - 1; + } else if (nextIndex > $items.length - 1) { + nextIndex = 0; + } + + this._selectedResultIndex = nextIndex; + + $items.removeClass("selected"); + + const $selectedItem: JQuery = $items.eq(this._selectedResultIndex); + + $selectedItem.addClass("selected"); + + const top = $selectedItem.outerHeight(true) * this._selectedResultIndex; + + this._$searchResultsList.scrollTop(top); + } + + private _search(term: string): void { + this._results = []; + + this._clearResults(); + this._showResults(); + this._$searchResultsList.append('
  • '); + + this._updateListPosition(); + + const that = this; + + this._autoCompleteFunc(term, (results: string[]) => { + that._listResults(results); + }); + } + + private _clearResults(): void { + this._$searchResultsList.empty(); + } + + private _hideResults(): void { + this._$searchResultsList.hide(); + } + + private _showResults(): void { + this._selectedResultIndex = -1; + this._$searchResultsList.show(); + } + + private _updateListPosition(): void { + if (this._positionAbove) { + this._$searchResultsList.css({ + top: this._$searchResultsList.outerHeight(true) * -1, + }); + } else { + this._$searchResultsList.css({ + top: this._$element.outerHeight(true), + }); + } + } + + private _listResults(results: string[]): void { + // get an array of strings + this._results = this._parseResultsFunc(results); + + this._clearResults(); + + if (!this._results.length) { + // don't do this, because there still may be results for the PHRASE but not the word. + // they won't know until they do the search. + //this.searchResultsList.append('
  • no results
  • '); + this._hideResults(); + return; + } + + for (let i = 0; i < this._results.length; i++) { + const result = this._results[i]; + const $resultItem = this._$searchResultTemplate.clone(); + const $a = $resultItem.find("a"); + $a.text(result); + this._$searchResultsList.append($resultItem); + } + + this._updateListPosition(); + + const that = this; + + const $listItems = this._$searchResultsList.find("li"); + + $listItems.each((_idx, item) => { + $(item).on("click", function(e: any) { + e.preventDefault(); + that._searchForItem($(this)); + }); + }); + } + + private _searchForItem($item: JQuery): void { + const term: string = $item.find("a").text(); + this._$element.val(term); + this._hideResults(); + this._onSelect(term); + this._clearResults(); + this._hideResults(); + } + + private _getSelectedListItem() { + return this._$searchResultsList.find("li.selected"); + } +} diff --git a/src/content-handlers/iiif/modules/uv-shared-module/PagingAutoComplete.ts b/src/content-handlers/iiif/modules/uv-shared-module/PagingAutoComplete.ts new file mode 100644 index 000000000..59c6cc0b1 --- /dev/null +++ b/src/content-handlers/iiif/modules/uv-shared-module/PagingAutoComplete.ts @@ -0,0 +1,296 @@ +const $ = require("jquery"); +import * as KeyCodes from "@edsilv/key-codes"; +import { Keyboard } from "@edsilv/utils"; +import { isVisible } from "../../../../Utils"; + +export class PagingAutoComplete { + private _results: any; + private _selectedResultIndex: number; + private _$element: JQuery; + private _autoCompleteFunc: ( + terms: string, + cb: (results: string[]) => void + ) => void; + private _delay: number; + private _minChars: number; + private _onSelect: (terms: string) => void; + private _parseResultsFunc: (results: string[]) => string[]; + private _positionAbove: boolean; + private _allowWords: boolean; + + private _$searchResultsList: JQuery; + private _$searchResultTemplate: JQuery; + + constructor( + element: JQuery, + autoCompleteFunc: (terms: string, cb: (results: string[]) => void) => void, + parseResultsFunc: (results: any) => string[], + onSelect: (terms: string) => void, + delay: number = 300, + minChars: number = 2, + positionAbove: boolean = false, + allowWords: boolean = false, + ) { + this._$element = element; + this._autoCompleteFunc = autoCompleteFunc; + this._delay = delay; + this._minChars = minChars; + this._onSelect = onSelect; + this._parseResultsFunc = parseResultsFunc; + this._positionAbove = positionAbove; + this._allowWords = allowWords; + + // create ui. + this._$searchResultsList = $(''); + + if (this._positionAbove) { + this._$element.parent().prepend(this._$searchResultsList); + } else { + this._$element.parent().append(this._$searchResultsList); + } + + this._$searchResultTemplate = $( + '
  • ' + ); + + // init ui. + + // callback after set period. + const typewatch = (function() { + let timer: number = 0; + return function(cb: Function, ms: number) { + clearTimeout(timer); + timer = setTimeout(cb, ms); + }; + })(); + + const that = this; + + this._$element.on("keydown", function(e: JQueryEventObject) { + const originalEvent: KeyboardEvent = e.originalEvent; + //that._lastKeyDownWasNavigation = that._isNavigationKeyDown(originalEvent); + const charCode: number = Keyboard.getCharCode(originalEvent); + let cancelEvent: boolean = false; + + if (charCode === KeyCodes.KeyDown.LeftArrow) { + cancelEvent = true; + } else if (charCode === KeyCodes.KeyDown.RightArrow) { + cancelEvent = true; + } + + if (cancelEvent) { + originalEvent.cancelBubble = true; + if (originalEvent.stopPropagation) originalEvent.stopPropagation(); + } + }); + + // this._$element.on("blur", () => { + // that._clearResults(); + // that._hideResults(); + // }); + + // auto complete + this._$element.on("keyup", function(e) { + // if pressing enter without a list item selected + if ( + !that._getSelectedListItem().length && + e.keyCode === KeyCodes.KeyDown.Enter + ) { + // enter + that._onSelect(that._getTerms()); + return; + } + + if (e.keyCode === KeyCodes.KeyDown.Tab) { + that._clearResults(); + that._hideResults(); + return; + } + + // If there are search results + if (isVisible(that._$searchResultsList) && that._results.length) { + if (e.keyCode === KeyCodes.KeyDown.Enter) { + that._searchForItem(that._getSelectedListItem()); + } else if (e.keyCode === KeyCodes.KeyDown.DownArrow) { + that._setSelectedResultIndex(1); + return; + } else if (e.keyCode === KeyCodes.KeyDown.UpArrow) { + that._setSelectedResultIndex(-1); + return; + } + } + + if (e.keyCode !== KeyCodes.KeyDown.Enter) { + // after a delay, show autocomplete list. + typewatch(() => { + const val = that._getTerms(); + that._search(val) + + // if there are more than x chars + // update the autocomplete list. + if (val && val.length > that._minChars && that._searchForWords(val)) { + that._search(val); + } else { + // otherwise, hide the autocomplete list. + that._clearResults(); + that._hideResults(); + } + }, that._delay); + } + }); + + //empty input if clicked/focussed on (and give all canvase labels as results) + this._$element.on("focusin", (e) => { + this._$element.val(''); + this._search('') + }); + + this._$element.parent().on("blur", (e) => { + this._clearResults(); + this._hideResults(); + }); + + // // hide results if clicked outside. + $(document).on("click", (e) => { + if (this._$searchResultsList.parent().has($(e.target)[0]).length === 0) { + this._clearResults(); + this._hideResults(); + } + }); + + + } + + private _searchForWords(search: string): boolean { + if (this._allowWords || !search.includes(" ")) { + return true; + } else { + return false; + } + } + + private _getTerms(): string { + return this._$element.val().trim(); + } + + private _setSelectedResultIndex(direction: number): void { + let nextIndex: number; + + if (direction === 1) { + nextIndex = this._selectedResultIndex + 1; + } else { + nextIndex = this._selectedResultIndex - 1; + } + + const $items: JQuery = this._$searchResultsList.find("li"); + + if (nextIndex < 0) { + nextIndex = $items.length - 1; + } else if (nextIndex > $items.length - 1) { + nextIndex = 0; + } + + this._selectedResultIndex = nextIndex; + + $items.removeClass("selected"); + + const $selectedItem: JQuery = $items.eq(this._selectedResultIndex); + + $selectedItem.addClass("selected"); + + const top = $selectedItem.outerHeight(true) * this._selectedResultIndex; + + this._$searchResultsList.scrollTop(top); + } + + private _search(term: string): void { + this._results = []; + + this._clearResults(); + this._showResults(); + this._$searchResultsList.append('
  • '); + + this._updateListPosition(); + + const that = this; + + this._autoCompleteFunc(term, (results: string[]) => { + that._listResults(results); + }); + } + + private _clearResults(): void { + this._$searchResultsList.empty(); + } + + private _hideResults(): void { + this._$searchResultsList.hide(); + } + + private _showResults(): void { + this._selectedResultIndex = -1; + this._$searchResultsList.show(); + } + + private _updateListPosition(): void { + if (this._positionAbove) { + this._$searchResultsList.css({ + top: this._$searchResultsList.outerHeight(true) * -1, + }); + } else { + this._$searchResultsList.css({ + top: this._$element.outerHeight(true), + }); + } + } + + private _listResults(results: string[]): void { + // get an array of strings + this._results = this._parseResultsFunc(results); + + this._clearResults(); + + if (!this._results.length) { + // don't do this, because there still may be results for the PHRASE but not the word. + // they won't know until they do the search. + //this.searchResultsList.append('
  • no results
  • '); + this._hideResults(); + return; + } + + for (let i = 0; i < this._results.length; i++) { + const result = this._results[i]; + const $resultItem = this._$searchResultTemplate.clone(); + const $a = $resultItem.find("a"); + $a.text(result); + this._$searchResultsList.append($resultItem); + } + + this._updateListPosition(); + + const that = this; + + const $listItems = this._$searchResultsList.find("li"); + + $listItems.each((_idx, item) => { + $(item).on("click", function(e: any) { + e.preventDefault(); + that._searchForItem($(this)); + }); + }); + } + + private _searchForItem($item: JQuery): void { + const term: string = $item.find("a").text(); + this._$element.val(term); + this._hideResults(); + this._onSelect(term); + this._clearResults(); + this._hideResults(); + } + + private _getSelectedListItem() { + return this._$searchResultsList.find("li.selected"); + } + +}