diff --git a/main.js b/main.js index 64d80908..020d4780 100644 --- a/main.js +++ b/main.js @@ -130,15 +130,19 @@ app.on("ready", () => { ipcMain.on("user-data", (event, arg) => { event.returnValue = dirPath; }); - ipcMain.on("close-reader", (event, arg) => { + ipcMain.handle("hide-reader", (event, arg) => { if (readerWindow && readerWindow.isFocused()) { - readerWindow.destroy(); + readerWindow.minimize(); + event.returnvalue = true; + } else if (mainWin && mainWin.isFocused()) { + mainWin.minimize(); + event.returnvalue = true; + } else { + event.returnvalue = false; } - event.returnvalue = false; }); ipcMain.on("switch-moyu", (event, arg) => { let id; - console.log(readerWindow); if (store.get("isPreventSleep") === "yes") { id = powerSaveBlocker.start("prevent-display-sleep"); console.log(powerSaveBlocker.isStarted(id)); diff --git a/public/lib/Rangy/rangy-highlighter.js b/public/lib/Rangy/rangy-highlighter.js index bb71d8d2..0c5a36cb 100644 --- a/public/lib/Rangy/rangy-highlighter.js +++ b/public/lib/Rangy/rangy-highlighter.js @@ -9,612 +9,776 @@ * Version: 1.3.0 * Build date: 10 May 2015 */ -(function(factory, root) { - if (typeof define == "function" && define.amd) { - // AMD. Register as an anonymous module with a dependency on Rangy. - define(["./rangy-core"], factory); - } else if (typeof module != "undefined" && typeof exports == "object") { - // Node/CommonJS style - module.exports = factory( require("rangy") ); - } else { - // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) - factory(root.rangy); +(function (factory, root) { + if (typeof define == "function" && define.amd) { + // AMD. Register as an anonymous module with a dependency on Rangy. + define(["./rangy-core"], factory); + } else if (typeof module != "undefined" && typeof exports == "object") { + // Node/CommonJS style + module.exports = factory(require("rangy")); + } else { + // No AMD or CommonJS support so we use the rangy property of root (probably the global variable) + factory(root.rangy); + } +})(function (rangy) { + rangy.createModule("Highlighter", ["ClassApplier"], function (api, module) { + var dom = api.dom; + var contains = dom.arrayContains; + var getBody = dom.getBody; + var createOptions = api.util.createOptions; + var forEach = api.util.forEach; + var nextHighlightId = 1; + + // Puts highlights in order, last in document first. + function compareHighlights(h1, h2) { + return h1.characterRange.start - h2.characterRange.start; } -})(function(rangy) { - rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) { - var dom = api.dom; - var contains = dom.arrayContains; - var getBody = dom.getBody; - var createOptions = api.util.createOptions; - var forEach = api.util.forEach; - var nextHighlightId = 1; - // Puts highlights in order, last in document first. - function compareHighlights(h1, h2) { - return h1.characterRange.start - h2.characterRange.start; + function getContainerElement(doc, id) { + return id ? doc.getElementById(id) : getBody(doc); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + var highlighterTypes = {}; + + function HighlighterType(type, converterCreator) { + this.type = type; + this.converterCreator = converterCreator; + } + + HighlighterType.prototype.create = function () { + var converter = this.converterCreator(); + converter.type = this.type; + return converter; + }; + + function registerHighlighterType(type, converterCreator) { + highlighterTypes[type] = new HighlighterType(type, converterCreator); + } + + function getConverter(type) { + var highlighterType = highlighterTypes[type]; + if (highlighterType instanceof HighlighterType) { + return highlighterType.create(); + } else { + throw new Error("Highlighter type '" + type + "' is not valid"); + } + } + + api.registerHighlighterType = registerHighlighterType; + + /*----------------------------------------------------------------------------------------------------------------*/ + + function CharacterRange(start, end) { + this.start = start; + this.end = end; + } + + CharacterRange.prototype = { + intersects: function (charRange) { + return this.start < charRange.end && this.end > charRange.start; + }, + + isContiguousWith: function (charRange) { + return this.start == charRange.end || this.end == charRange.start; + }, + + union: function (charRange) { + return new CharacterRange( + Math.min(this.start, charRange.start), + Math.max(this.end, charRange.end) + ); + }, + + intersection: function (charRange) { + return new CharacterRange( + Math.max(this.start, charRange.start), + Math.min(this.end, charRange.end) + ); + }, + + getComplements: function (charRange) { + var ranges = []; + if (this.start >= charRange.start) { + if (this.end <= charRange.end) { + return []; + } + ranges.push(new CharacterRange(charRange.end, this.end)); + } else { + ranges.push( + new CharacterRange(this.start, Math.min(this.end, charRange.start)) + ); + if (this.end > charRange.end) { + ranges.push(new CharacterRange(charRange.end, this.end)); + } } + return ranges; + }, - function getContainerElement(doc, id) { - return id ? doc.getElementById(id) : getBody(doc); - } + toString: function () { + return "[CharacterRange(" + this.start + ", " + this.end + ")]"; + }, + }; - /*----------------------------------------------------------------------------------------------------------------*/ + CharacterRange.fromCharacterRange = function (charRange) { + return new CharacterRange(charRange.start, charRange.end); + }; - var highlighterTypes = {}; + /*----------------------------------------------------------------------------------------------------------------*/ - function HighlighterType(type, converterCreator) { - this.type = type; - this.converterCreator = converterCreator; - } + var textContentConverter = { + rangeToCharacterRange: function (range, containerNode) { + var bookmark = range.getBookmark(containerNode); + return new CharacterRange(bookmark.start, bookmark.end); + }, - HighlighterType.prototype.create = function() { - var converter = this.converterCreator(); - converter.type = this.type; - return converter; - }; - - function registerHighlighterType(type, converterCreator) { - highlighterTypes[type] = new HighlighterType(type, converterCreator); - } - - function getConverter(type) { - var highlighterType = highlighterTypes[type]; - if (highlighterType instanceof HighlighterType) { - return highlighterType.create(); - } else { - throw new Error("Highlighter type '" + type + "' is not valid"); - } - } - - api.registerHighlighterType = registerHighlighterType; - - /*----------------------------------------------------------------------------------------------------------------*/ - - function CharacterRange(start, end) { - this.start = start; - this.end = end; - } - - CharacterRange.prototype = { - intersects: function(charRange) { - return this.start < charRange.end && this.end > charRange.start; - }, - - isContiguousWith: function(charRange) { - return this.start == charRange.end || this.end == charRange.start; - }, - - union: function(charRange) { - return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end)); - }, - - intersection: function(charRange) { - return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end)); - }, - - getComplements: function(charRange) { - var ranges = []; - if (this.start >= charRange.start) { - if (this.end <= charRange.end) { - return []; - } - ranges.push(new CharacterRange(charRange.end, this.end)); - } else { - ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start))); - if (this.end > charRange.end) { - ranges.push(new CharacterRange(charRange.end, this.end)); - } - } - return ranges; - }, - - toString: function() { - return "[CharacterRange(" + this.start + ", " + this.end + ")]"; - } - }; - - CharacterRange.fromCharacterRange = function(charRange) { - return new CharacterRange(charRange.start, charRange.end); - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - var textContentConverter = { - rangeToCharacterRange: function(range, containerNode) { - var bookmark = range.getBookmark(containerNode); - return new CharacterRange(bookmark.start, bookmark.end); - }, - - characterRangeToRange: function(doc, characterRange, containerNode) { - var range = api.createRange(doc); - range.moveToBookmark({ - start: characterRange.start, - end: characterRange.end, - containerNode: containerNode - }); - - return range; - }, - - serializeSelection: function(selection, containerNode) { - var ranges = selection.getAllRanges(), rangeCount = ranges.length; - var rangeInfos = []; - - var backward = rangeCount == 1 && selection.isBackward(); - - for (var i = 0, len = ranges.length; i < len; ++i) { - rangeInfos[i] = { - characterRange: this.rangeToCharacterRange(ranges[i], containerNode), - backward: backward - }; - } - - return rangeInfos; - }, - - restoreSelection: function(selection, savedSelection, containerNode) { - selection.removeAllRanges(); - var doc = selection.win.document; - for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) { - rangeInfo = savedSelection[i]; - characterRange = rangeInfo.characterRange; - range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode); - selection.addRange(range, rangeInfo.backward); - } - } - }; - - registerHighlighterType("textContent", function() { - return textContentConverter; + characterRangeToRange: function (doc, characterRange, containerNode) { + var range = api.createRange(doc); + range.moveToBookmark({ + start: characterRange.start, + end: characterRange.end, + containerNode: containerNode, }); - /*----------------------------------------------------------------------------------------------------------------*/ + return range; + }, - // Lazily load the TextRange-based converter so that the dependency is only checked when required. - registerHighlighterType("TextRange", (function() { - var converter; + serializeSelection: function (selection, containerNode) { + var ranges = selection.getAllRanges(), + rangeCount = ranges.length; + var rangeInfos = []; - return function() { - if (!converter) { - // Test that textRangeModule exists and is supported - var textRangeModule = api.modules.TextRange; - if (!textRangeModule) { - throw new Error("TextRange module is missing."); - } else if (!textRangeModule.supported) { - throw new Error("TextRange module is present but not supported."); - } + var backward = rangeCount == 1 && selection.isBackward(); - converter = { - rangeToCharacterRange: function(range, containerNode) { - return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) ); - }, - - characterRangeToRange: function(doc, characterRange, containerNode) { - var range = api.createRange(doc); - range.selectCharacters(containerNode, characterRange.start, characterRange.end); - return range; - }, - - serializeSelection: function(selection, containerNode) { - return selection.saveCharacterRanges(containerNode); - }, - - restoreSelection: function(selection, savedSelection, containerNode) { - selection.restoreCharacterRanges(containerNode, savedSelection); - } - }; - } - - return converter; - }; - })()); - - /*----------------------------------------------------------------------------------------------------------------*/ - - function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) { - if (id) { - this.id = id; - nextHighlightId = Math.max(nextHighlightId, id + 1); - } else { - this.id = nextHighlightId++; - } - this.characterRange = characterRange; - this.doc = doc; - this.classApplier = classApplier; - this.converter = converter; - this.containerElementId = containerElementId || null; - this.applied = false; + for (var i = 0, len = ranges.length; i < len; ++i) { + rangeInfos[i] = { + characterRange: this.rangeToCharacterRange( + ranges[i], + containerNode + ), + backward: backward, + }; } - Highlight.prototype = { - getContainerElement: function() { - return getContainerElement(this.doc, this.containerElementId); - }, + return rangeInfos; + }, - getRange: function() { - return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement()); - }, - - fromRange: function(range) { - this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement()); - }, - - getText: function() { - return this.getRange().toString(); - }, - - containsElement: function(el) { - return this.getRange().containsNodeContents(el.firstChild); - }, - - unapply: function() { - this.classApplier.undoToRange(this.getRange()); - this.applied = false; - }, - - apply: function() { - this.classApplier.applyToRange(this.getRange()); - this.applied = true; - }, - - getHighlightElements: function() { - return this.classApplier.getElementsWithClassIntersectingRange(this.getRange()); - }, - - toString: function() { - return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " + - this.characterRange.start + " - " + this.characterRange.end + ")]"; - } - }; - - /*----------------------------------------------------------------------------------------------------------------*/ - - function Highlighter(doc, type) { - type = type || "textContent"; - this.doc = doc || document; - this.classAppliers = {}; - this.highlights = []; - this.converter = getConverter(type); + restoreSelection: function (selection, savedSelection, containerNode) { + selection.removeAllRanges(); + var doc = selection.win.document; + for ( + var i = 0, + len = savedSelection.length, + range, + rangeInfo, + characterRange; + i < len; + ++i + ) { + rangeInfo = savedSelection[i]; + characterRange = rangeInfo.characterRange; + range = this.characterRangeToRange( + doc, + rangeInfo.characterRange, + containerNode + ); + selection.addRange(range, rangeInfo.backward); } + }, + }; - Highlighter.prototype = { - addClassApplier: function(classApplier) { - this.classAppliers[classApplier.className] = classApplier; - }, - - getHighlightForElement: function(el) { - var highlights = this.highlights; - for (var i = 0, len = highlights.length; i < len; ++i) { - if (highlights[i].containsElement(el)) { - return highlights[i]; - } - } - return null; - }, - - removeHighlights: function(highlights) { - for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) { - highlight = this.highlights[i]; - if (contains(highlights, highlight)) { - highlight.unapply(); - this.highlights.splice(i--, 1); - } - } - }, - - removeAllHighlights: function() { - this.removeHighlights(this.highlights); - }, - - getIntersectingHighlights: function(ranges) { - // Test each range against each of the highlighted ranges to see whether they overlap - var intersectingHighlights = [], highlights = this.highlights; - forEach(ranges, function(range) { - //var selCharRange = converter.rangeToCharacterRange(range); - forEach(highlights, function(highlight) { - if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) { - intersectingHighlights.push(highlight); - } - }); - }); - - return intersectingHighlights; - }, - - highlightCharacterRanges: function(className, charRanges, options) { - var i, len, j; - var highlights = this.highlights; - var converter = this.converter; - var doc = this.doc; - var highlightsToRemove = []; - var classApplier = className ? this.classAppliers[className] : null; - - options = createOptions(options, { - containerElementId: null, - exclusive: true - }); - - var containerElementId = options.containerElementId; - var exclusive = options.exclusive; - - var containerElement, containerElementRange, containerElementCharRange; - if (containerElementId) { - containerElement = this.doc.getElementById(containerElementId); - if (containerElement) { - containerElementRange = api.createRange(this.doc); - containerElementRange.selectNodeContents(containerElement); - containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length); - } - } - - var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight; - - for (i = 0, len = charRanges.length; i < len; ++i) { - charRange = charRanges[i]; - highlightsToKeep = []; - - // Restrict character range to container element, if it exists - if (containerElementCharRange) { - charRange = charRange.intersection(containerElementCharRange); - } - - // Ignore empty ranges - if (charRange.start == charRange.end) { - continue; - } - - // Check for intersection with existing highlights. For each intersection, create a new highlight - // which is the union of the highlight range and the selected range - for (j = 0; j < highlights.length; ++j) { - removeHighlight = false; - - if (containerElementId == highlights[j].containerElementId) { - highlightCharRange = highlights[j].characterRange; - isSameClassApplier = (classApplier == highlights[j].classApplier); - splitHighlight = !isSameClassApplier && exclusive; - - // Replace the existing highlight if it needs to be: - // 1. merged (isSameClassApplier) - // 2. partially or entirely erased (className === null) - // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true) - if ( (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) && - (isSameClassApplier || splitHighlight) ) { - - // Remove existing highlights, keeping the unselected parts - if (splitHighlight) { - forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) { - highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) ); - }); - } - - removeHighlight = true; - if (isSameClassApplier) { - charRange = highlightCharRange.union(charRange); - } - } - } - - if (removeHighlight) { - highlightsToRemove.push(highlights[j]); - highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId); - } else { - highlightsToKeep.push(highlights[j]); - } - } - - // Add new range - if (classApplier) { - highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId)); - } - this.highlights = highlights = highlightsToKeep; - } - - // Remove the old highlights - forEach(highlightsToRemove, function(highlightToRemove) { - highlightToRemove.unapply(); - }); - - // Apply new highlights - var newHighlights = []; - forEach(highlights, function(highlight) { - if (!highlight.applied) { - highlight.apply(); - newHighlights.push(highlight); - } - }); - - return newHighlights; - }, - - highlightRanges: function(className, ranges, options) { - var selCharRanges = []; - var converter = this.converter; - - options = createOptions(options, { - containerElement: null, - exclusive: true - }); - - var containerElement = options.containerElement; - var containerElementId = containerElement ? containerElement.id : null; - var containerElementRange; - if (containerElement) { - containerElementRange = api.createRange(containerElement); - containerElementRange.selectNodeContents(containerElement); - } - - forEach(ranges, function(range) { - var scopedRange = containerElement ? containerElementRange.intersection(range) : range; - selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) ); - }); - - return this.highlightCharacterRanges(className, selCharRanges, { - containerElementId: containerElementId, - exclusive: options.exclusive - }); - }, - - highlightSelection: function(className, options) { - var converter = this.converter; - var classApplier = className ? this.classAppliers[className] : false; - - options = createOptions(options, { - containerElementId: null, - selection: api.getSelection(this.doc), - exclusive: true - }); - - var containerElementId = options.containerElementId; - var exclusive = options.exclusive; - var selection = options.selection; - var doc = selection.win.document; - var containerElement = getContainerElement(doc, containerElementId); - - if (!classApplier && className !== false) { - throw new Error("No class applier found for class '" + className + "'"); - } - - // Store the existing selection as character ranges - var serializedSelection = converter.serializeSelection(selection, containerElement); - - // Create an array of selected character ranges - var selCharRanges = []; - forEach(serializedSelection, function(rangeInfo) { - selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) ); - }); - - var newHighlights = this.highlightCharacterRanges(className, selCharRanges, { - containerElementId: containerElementId, - exclusive: exclusive - }); - - // Restore selection - converter.restoreSelection(selection, serializedSelection, containerElement); - - return newHighlights; - }, - - unhighlightSelection: function(selection) { - selection = selection || api.getSelection(this.doc); - var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() ); - this.removeHighlights(intersectingHighlights); - selection.removeAllRanges(); - return intersectingHighlights; - }, - - getHighlightsInSelection: function(selection) { - selection = selection || api.getSelection(this.doc); - return this.getIntersectingHighlights(selection.getAllRanges()); - }, - - selectionOverlapsHighlight: function(selection) { - return this.getHighlightsInSelection(selection).length > 0; - }, - - serialize: function(options) { - var highlighter = this; - var highlights = highlighter.highlights; - var serializedType, serializedHighlights, convertType, serializationConverter; - - highlights.sort(compareHighlights); - options = createOptions(options, { - serializeHighlightText: false, - type: highlighter.converter.type - }); - - serializedType = options.type; - convertType = (serializedType != highlighter.converter.type); - - if (convertType) { - serializationConverter = getConverter(serializedType); - } - - serializedHighlights = ["type:" + serializedType]; - - forEach(highlights, function(highlight) { - var characterRange = highlight.characterRange; - var containerElement; - - // Convert to the current Highlighter's type, if different from the serialization type - if (convertType) { - containerElement = highlight.getContainerElement(); - characterRange = serializationConverter.rangeToCharacterRange( - highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement), - containerElement - ); - } - - var parts = [ - characterRange.start, - characterRange.end, - highlight.id, - highlight.classApplier.className, - highlight.containerElementId - ]; - - if (options.serializeHighlightText) { - parts.push(highlight.getText()); - } - serializedHighlights.push( parts.join("$") ); - }); - - return serializedHighlights.join("|"); - }, - - deserialize: function(serialized) { - var serializedHighlights = serialized.split("|"); - var highlights = []; - - var firstHighlight = serializedHighlights[0]; - var regexResult; - var serializationType, serializationConverter, convertType = false; - if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) { - serializationType = regexResult[1]; - if (serializationType != this.converter.type) { - serializationConverter = getConverter(serializationType); - convertType = true; - } - serializedHighlights.shift(); - } else { - throw new Error("Serialized highlights are invalid."); - } - - var classApplier, highlight, characterRange, containerElementId, containerElement; - - for (var i = serializedHighlights.length, parts; i-- > 0; ) { - parts = serializedHighlights[i].split("$"); - characterRange = new CharacterRange(+parts[0], +parts[1]); - containerElementId = parts[4] || null; - - // Convert to the current Highlighter's type, if different from the serialization type - if (convertType) { - containerElement = getContainerElement(this.doc, containerElementId); - characterRange = this.converter.rangeToCharacterRange( - serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement), - containerElement - ); - } - - classApplier = this.classAppliers[ parts[3] ]; - - if (!classApplier) { - throw new Error("No class applier found for class '" + parts[3] + "'"); - } - - highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId); - highlight.apply(); - highlights.push(highlight); - } - this.highlights = highlights; - } - }; - - api.Highlighter = Highlighter; - - api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) { - return new Highlighter(doc, rangeCharacterOffsetConverterType); - }; + registerHighlighterType("textContent", function () { + return textContentConverter; }); - - return rangy; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Lazily load the TextRange-based converter so that the dependency is only checked when required. + registerHighlighterType( + "TextRange", + (function () { + var converter; + + return function () { + if (!converter) { + // Test that textRangeModule exists and is supported + var textRangeModule = api.modules.TextRange; + if (!textRangeModule) { + throw new Error("TextRange module is missing."); + } else if (!textRangeModule.supported) { + throw new Error("TextRange module is present but not supported."); + } + + converter = { + rangeToCharacterRange: function (range, containerNode) { + return CharacterRange.fromCharacterRange( + range.toCharacterRange(containerNode) + ); + }, + + characterRangeToRange: function ( + doc, + characterRange, + containerNode + ) { + var range = api.createRange(doc); + range.selectCharacters( + containerNode, + characterRange.start, + characterRange.end + ); + return range; + }, + + serializeSelection: function (selection, containerNode) { + return selection.saveCharacterRanges(containerNode); + }, + + restoreSelection: function ( + selection, + savedSelection, + containerNode + ) { + selection.restoreCharacterRanges(containerNode, savedSelection); + }, + }; + } + + return converter; + }; + })() + ); + + /*----------------------------------------------------------------------------------------------------------------*/ + + function Highlight( + doc, + characterRange, + classApplier, + converter, + id, + containerElementId + ) { + if (id) { + this.id = id; + nextHighlightId = Math.max(nextHighlightId, id + 1); + } else { + this.id = nextHighlightId++; + } + this.characterRange = characterRange; + this.doc = doc; + this.classApplier = classApplier; + this.converter = converter; + this.containerElementId = containerElementId || null; + this.applied = false; + } + + Highlight.prototype = { + getContainerElement: function () { + return getContainerElement(this.doc, this.containerElementId); + }, + + getRange: function () { + return this.converter.characterRangeToRange( + this.doc, + this.characterRange, + this.getContainerElement() + ); + }, + + fromRange: function (range) { + this.characterRange = this.converter.rangeToCharacterRange( + range, + this.getContainerElement() + ); + }, + + getText: function () { + return this.getRange().toString(); + }, + + containsElement: function (el) { + return this.getRange().containsNodeContents(el.firstChild); + }, + + unapply: function () { + this.classApplier.undoToRange(this.getRange()); + this.applied = false; + }, + + apply: function () { + this.classApplier.applyToRange(this.getRange()); + this.applied = true; + }, + + getHighlightElements: function () { + return this.classApplier.getElementsWithClassIntersectingRange( + this.getRange() + ); + }, + + toString: function () { + return ( + "[Highlight(ID: " + + this.id + + ", class: " + + this.classApplier.className + + ", character range: " + + this.characterRange.start + + " - " + + this.characterRange.end + + ")]" + ); + }, + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + function Highlighter(doc, type) { + type = type || "textContent"; + this.doc = doc || document; + this.classAppliers = {}; + this.highlights = []; + this.converter = getConverter(type); + } + + Highlighter.prototype = { + addClassApplier: function (classApplier) { + this.classAppliers[classApplier.className] = classApplier; + }, + + getHighlightForElement: function (el) { + var highlights = this.highlights; + for (var i = 0, len = highlights.length; i < len; ++i) { + if (highlights[i].containsElement(el)) { + return highlights[i]; + } + } + return null; + }, + + removeHighlights: function (highlights) { + for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) { + highlight = this.highlights[i]; + if (contains(highlights, highlight)) { + highlight.unapply(); + this.highlights.splice(i--, 1); + } + } + }, + + removeAllHighlights: function () { + this.removeHighlights(this.highlights); + }, + + getIntersectingHighlights: function (ranges) { + // Test each range against each of the highlighted ranges to see whether they overlap + var intersectingHighlights = [], + highlights = this.highlights; + forEach(ranges, function (range) { + //var selCharRange = converter.rangeToCharacterRange(range); + forEach(highlights, function (highlight) { + if ( + range.intersectsRange(highlight.getRange()) && + !contains(intersectingHighlights, highlight) + ) { + intersectingHighlights.push(highlight); + } + }); + }); + + return intersectingHighlights; + }, + + highlightCharacterRanges: function (className, charRanges, options) { + var i, len, j; + var highlights = this.highlights; + var converter = this.converter; + var doc = this.doc; + var highlightsToRemove = []; + var classApplier = className ? this.classAppliers[className] : null; + + options = createOptions(options, { + containerElementId: null, + exclusive: true, + }); + + var containerElementId = options.containerElementId; + var exclusive = options.exclusive; + + var containerElement, containerElementRange, containerElementCharRange; + if (containerElementId) { + containerElement = this.doc.getElementById(containerElementId); + if (containerElement) { + containerElementRange = api.createRange(this.doc); + containerElementRange.selectNodeContents(containerElement); + containerElementCharRange = new CharacterRange( + 0, + containerElementRange.toString().length + ); + } + } + + var charRange, + highlightCharRange, + removeHighlight, + isSameClassApplier, + highlightsToKeep, + splitHighlight; + + for (i = 0, len = charRanges.length; i < len; ++i) { + charRange = charRanges[i]; + highlightsToKeep = []; + + // Restrict character range to container element, if it exists + if (containerElementCharRange) { + charRange = charRange.intersection(containerElementCharRange); + } + + // Ignore empty ranges + if (charRange.start == charRange.end) { + continue; + } + + // Check for intersection with existing highlights. For each intersection, create a new highlight + // which is the union of the highlight range and the selected range + for (j = 0; j < highlights.length; ++j) { + removeHighlight = false; + + if (containerElementId == highlights[j].containerElementId) { + highlightCharRange = highlights[j].characterRange; + isSameClassApplier = classApplier == highlights[j].classApplier; + splitHighlight = !isSameClassApplier && exclusive; + + // Replace the existing highlight if it needs to be: + // 1. merged (isSameClassApplier) + // 2. partially or entirely erased (className === null) + // 3. partially or entirely replaced (isSameClassApplier == false && exclusive == true) + if ( + (highlightCharRange.intersects(charRange) || + highlightCharRange.isContiguousWith(charRange)) && + (isSameClassApplier || splitHighlight) + ) { + // Remove existing highlights, keeping the unselected parts + if (splitHighlight) { + forEach( + highlightCharRange.getComplements(charRange), + function (rangeToAdd) { + highlightsToKeep.push( + new Highlight( + doc, + rangeToAdd, + highlights[j].classApplier, + converter, + null, + containerElementId + ) + ); + } + ); + } + + removeHighlight = true; + if (isSameClassApplier) { + charRange = highlightCharRange.union(charRange); + } + } + } + + if (removeHighlight) { + highlightsToRemove.push(highlights[j]); + highlights[j] = new Highlight( + doc, + highlightCharRange.union(charRange), + classApplier, + converter, + null, + containerElementId + ); + } else { + highlightsToKeep.push(highlights[j]); + } + } + + // Add new range + if (classApplier) { + highlightsToKeep.push( + new Highlight( + doc, + charRange, + classApplier, + converter, + null, + containerElementId + ) + ); + } + this.highlights = highlights = highlightsToKeep; + } + + // Remove the old highlights + forEach(highlightsToRemove, function (highlightToRemove) { + highlightToRemove.unapply(); + }); + + // Apply new highlights + var newHighlights = []; + forEach(highlights, function (highlight) { + if (!highlight.applied) { + highlight.apply(); + newHighlights.push(highlight); + } + }); + + return newHighlights; + }, + + highlightRanges: function (className, ranges, options) { + var selCharRanges = []; + var converter = this.converter; + + options = createOptions(options, { + containerElement: null, + exclusive: true, + }); + + var containerElement = options.containerElement; + var containerElementId = containerElement ? containerElement.id : null; + var containerElementRange; + if (containerElement) { + containerElementRange = api.createRange(containerElement); + containerElementRange.selectNodeContents(containerElement); + } + + forEach(ranges, function (range) { + var scopedRange = containerElement + ? containerElementRange.intersection(range) + : range; + selCharRanges.push( + converter.rangeToCharacterRange( + scopedRange, + containerElement || getBody(range.getDocument()) + ) + ); + }); + + return this.highlightCharacterRanges(className, selCharRanges, { + containerElementId: containerElementId, + exclusive: options.exclusive, + }); + }, + + highlightSelection: function (className, options) { + var converter = this.converter; + var classApplier = className ? this.classAppliers[className] : false; + + options = createOptions(options, { + containerElementId: null, + selection: api.getSelection(this.doc), + exclusive: true, + }); + + var containerElementId = options.containerElementId; + var exclusive = options.exclusive; + var selection = options.selection; + var doc = selection.win.document; + var containerElement = getContainerElement(doc, containerElementId); + + if (!classApplier && className !== false) { + throw new Error( + "No class applier found for class '" + className + "'" + ); + } + + // Store the existing selection as character ranges + var serializedSelection = converter.serializeSelection( + selection, + containerElement + ); + + // Create an array of selected character ranges + var selCharRanges = []; + forEach(serializedSelection, function (rangeInfo) { + selCharRanges.push( + CharacterRange.fromCharacterRange(rangeInfo.characterRange) + ); + }); + + var newHighlights = this.highlightCharacterRanges( + className, + selCharRanges, + { + containerElementId: containerElementId, + exclusive: exclusive, + } + ); + + // Restore selection + converter.restoreSelection( + selection, + serializedSelection, + containerElement + ); + + return newHighlights; + }, + + unhighlightSelection: function (selection) { + selection = selection || api.getSelection(this.doc); + var intersectingHighlights = this.getIntersectingHighlights( + selection.getAllRanges() + ); + this.removeHighlights(intersectingHighlights); + selection.removeAllRanges(); + return intersectingHighlights; + }, + + getHighlightsInSelection: function (selection) { + selection = selection || api.getSelection(this.doc); + return this.getIntersectingHighlights(selection.getAllRanges()); + }, + + selectionOverlapsHighlight: function (selection) { + return this.getHighlightsInSelection(selection).length > 0; + }, + + serialize: function (options) { + var highlighter = this; + var highlights = highlighter.highlights; + var serializedType, + serializedHighlights, + convertType, + serializationConverter; + + highlights.sort(compareHighlights); + options = createOptions(options, { + serializeHighlightText: false, + type: highlighter.converter.type, + }); + + serializedType = options.type; + convertType = serializedType != highlighter.converter.type; + + if (convertType) { + serializationConverter = getConverter(serializedType); + } + + serializedHighlights = ["type:" + serializedType]; + + forEach(highlights, function (highlight) { + var characterRange = highlight.characterRange; + var containerElement; + + // Convert to the current Highlighter's type, if different from the serialization type + if (convertType) { + containerElement = highlight.getContainerElement(); + characterRange = serializationConverter.rangeToCharacterRange( + highlighter.converter.characterRangeToRange( + highlighter.doc, + characterRange, + containerElement + ), + containerElement + ); + } + + var parts = [ + characterRange.start, + characterRange.end, + highlight.id, + highlight.classApplier.className, + highlight.containerElementId, + ]; + + if (options.serializeHighlightText) { + parts.push(highlight.getText()); + } + serializedHighlights.push(parts.join("$")); + }); + + return serializedHighlights.join("|"); + }, + + deserialize: function (serialized) { + var serializedHighlights = serialized.split("|"); + var highlights = []; + + var firstHighlight = serializedHighlights[0]; + var regexResult; + var serializationType, + serializationConverter, + convertType = false; + if ( + firstHighlight && + (regexResult = /^type:(\w+)$/.exec(firstHighlight)) + ) { + serializationType = regexResult[1]; + if (serializationType != this.converter.type) { + serializationConverter = getConverter(serializationType); + convertType = true; + } + serializedHighlights.shift(); + } else { + throw new Error("Serialized highlights are invalid."); + } + + var classApplier, + highlight, + characterRange, + containerElementId, + containerElement; + + for (var i = serializedHighlights.length, parts; i-- > 0; ) { + parts = serializedHighlights[i].split("$"); + characterRange = new CharacterRange(+parts[0], +parts[1]); + containerElementId = parts[4] || null; + + // Convert to the current Highlighter's type, if different from the serialization type + if (convertType) { + containerElement = getContainerElement( + this.doc, + containerElementId + ); + characterRange = this.converter.rangeToCharacterRange( + serializationConverter.characterRangeToRange( + this.doc, + characterRange, + containerElement + ), + containerElement + ); + } + + classApplier = this.classAppliers[parts[3]]; + + if (!classApplier) { + throw new Error( + "No class applier found for class '" + parts[3] + "'" + ); + } + + highlight = new Highlight( + this.doc, + characterRange, + classApplier, + this.converter, + parseInt(parts[2]), + containerElementId + ); + highlight.apply(); + highlights.push(highlight); + } + this.highlights = highlights; + }, + }; + + api.Highlighter = Highlighter; + + api.createHighlighter = function (doc, rangeCharacterOffsetConverterType) { + return new Highlighter(doc, rangeCharacterOffsetConverterType); + }; + }); + + return rangy; }, this); diff --git a/src/components/popups/popupMenu/component.tsx b/src/components/popups/popupMenu/component.tsx index 8a14da7c..c90ad795 100644 --- a/src/components/popups/popupMenu/component.tsx +++ b/src/components/popups/popupMenu/component.tsx @@ -249,7 +249,6 @@ class PopupMenu extends React.Component { "line-2", "line-3", ]; - highlightersByChapter && highlightersByChapter.forEach((item: any) => { this.key = item.key; @@ -344,6 +343,7 @@ class PopupMenu extends React.Component { } render() { if (this.props.menuMode === "highlight") { + // this.getHighlighter(); this.handleHighlight(); } const PopupProps = { diff --git a/src/containers/panels/operationPanel/component.tsx b/src/containers/panels/operationPanel/component.tsx index 04da193a..5209c8d1 100644 --- a/src/containers/panels/operationPanel/component.tsx +++ b/src/containers/panels/operationPanel/component.tsx @@ -121,6 +121,9 @@ class OperationPanel extends React.Component< window.speechSynthesis.cancel(); ReadingTime.setTime(this.props.currentBook.key, this.props.time); this.handleExitFullScreen(); + if (this.props.htmlBook) { + this.props.handleHtmlBook(null); + } } //控制进入全屏 handleFullScreen() { diff --git a/src/containers/panels/operationPanel/index.tsx b/src/containers/panels/operationPanel/index.tsx index 47193e2f..08dd3ca6 100644 --- a/src/containers/panels/operationPanel/index.tsx +++ b/src/containers/panels/operationPanel/index.tsx @@ -6,6 +6,7 @@ import { handleShowBookmark, handleSearch, handleReadingState, + handleHtmlBook, } from "../../../store/actions"; import { stateType } from "../../../store"; import { withTranslation } from "react-i18next"; @@ -18,6 +19,7 @@ const mapStateToProps = (state: stateType) => { bookmarks: state.reader.bookmarks, notes: state.reader.notes, books: state.manager.books, + htmlBook: state.reader.htmlBook, locations: state.progressPanel.locations, flattenChapters: state.reader.flattenChapters, }; @@ -29,6 +31,7 @@ const actionCreator = { handleOpenMenu, handleShowBookmark, handleSearch, + handleHtmlBook, }; export default connect( mapStateToProps, diff --git a/src/containers/panels/operationPanel/interface.tsx b/src/containers/panels/operationPanel/interface.tsx index 01b3723e..a1cd2c65 100644 --- a/src/containers/panels/operationPanel/interface.tsx +++ b/src/containers/panels/operationPanel/interface.tsx @@ -1,5 +1,6 @@ import BookModel from "../../../model/Book"; import NoteModel from "../../../model/Note"; +import HtmlBookModel from "../../../model/HtmlBook"; import BookmarkModel from "../../../model/Bookmark"; import { RouteComponentProps } from "react-router"; @@ -9,6 +10,7 @@ export interface OperationPanelProps extends RouteComponentProps { bookmarks: BookmarkModel[]; notes: NoteModel[]; books: BookModel[]; + htmlBook: HtmlBookModel; flattenChapters: any; locations: any; rendition: any; @@ -20,6 +22,7 @@ export interface OperationPanelProps extends RouteComponentProps { handleOpenMenu: (isOpenMenu: boolean) => void; handleShowBookmark: (isShowBookmark: boolean) => void; t: (title: string) => string; + handleHtmlBook: (htmlBook: HtmlBookModel | null) => void; } export interface OperationPanelState { diff --git a/src/utils/mouseEvent.tsx b/src/utils/mouseEvent.tsx index 260e6cf5..243c12be 100644 --- a/src/utils/mouseEvent.tsx +++ b/src/utils/mouseEvent.tsx @@ -17,7 +17,8 @@ export const getSelection = () => { return text; }; let lock = false; //prevent from clicking too fast -const arrowKeys = (rendition: any, keyCode: number) => { +const arrowKeys = (rendition: any, keyCode: number, event: any) => { + event.preventDefault(); if (document.querySelector(".editor-box")) { return; } @@ -57,7 +58,7 @@ const arrowKeys = (rendition: any, keyCode: number) => { } if (keyCode === 9) { if (isElectron) { - window.require("electron").ipcRenderer.sendSync("close-reader", "ping"); + window.require("electron").ipcRenderer.invoke("hide-reader", "ping"); } lock = true; setTimeout(function () { @@ -114,7 +115,7 @@ const bindEvent = ( readerMode: string = "" ) => { doc.addEventListener("keydown", (event) => { - arrowKeys(rendition, event.keyCode); + arrowKeys(rendition, event.keyCode, event); //使用Key判断是否是htmlBook if (key) { let postion = rendition.getPosition(); @@ -184,7 +185,7 @@ export const EpubMouseEvent = (rendition: any) => { } // navigate with mousewheel window.addEventListener("keydown", (event) => { - arrowKeys(rendition, event.keyCode); + arrowKeys(rendition, event.keyCode, event); }); bindEvent(rendition, doc); }); @@ -203,7 +204,7 @@ export const HtmlMouseEvent = ( } // navigate with mousewheel window.addEventListener("keydown", (event) => { - arrowKeys(rendition, event.keyCode); + arrowKeys(rendition, event.keyCode, event); if (key) { let postion = rendition.getPosition(); RecordLocation.recordScrollHeight(