blob: 63acf123ebaa57473b3721652dd8826e876d2a4d [file] [log] [blame]
/**
* @license
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* global corpus, module, parsed, selectable */
import {
parse as parseFragment,
stringify as stringifyFragment,
} from '@annotator/fragment-identifier';
import {
createRangeSelectorCreator,
createTextQuoteSelector,
describeTextQuote,
} from '@annotator/dom';
import { makeRefinable } from '@annotator/selector';
function clear() {
corpus.innerHTML = selectable.innerHTML;
}
function highlight(range) {
for (const node of textNodes(range)) {
const mark = document.createElement('mark');
const markRange = document.createRange();
markRange.selectNode(node);
markRange.surroundContents(mark);
}
}
function textNodes(range) {
const nodes = [];
if (range.collapsed) return nodes;
let startNode = range.startContainer;
let startOffset = range.startOffset;
if (startNode.nodeType === 3) {
if (startOffset > 0 && startOffset < startNode.length) {
startNode = startNode.splitText(startOffset);
startOffset = 0;
}
}
let endNode = range.endContainer;
let endOffset = range.endOffset;
if (endNode.nodeType === 3) {
if (endOffset > 0 && endOffset < endNode.length) {
endNode = endNode.splitText(endOffset);
endOffset = 0;
}
}
const walker = document.createTreeWalker(document.documentElement);
walker.currentNode = startNode;
while (walker.currentNode !== endNode) {
if (walker.currentNode.nodeType === 3) {
nodes.push(walker.currentNode);
}
walker.nextNode();
}
if (endNode.nodeType === 3 && endOffset > 0) {
nodes.push(endNode);
}
return nodes;
}
const createSelector = makeRefinable(selector => {
const selectorCreator = {
TextQuoteSelector: createTextQuoteSelector,
RangeSelector: createRangeSelectorCreator(createSelector),
}[selector.type];
if (selectorCreator == null) {
throw new Error(`Unsupported selector type: ${selector.type}`);
}
return selectorCreator(selector);
});
const refresh = async () => {
clear();
const fragment = window.location.hash.slice(1);
if (!fragment) return;
const { selector } = parseFragment(fragment);
const matchAll = createSelector(selector);
const ranges = [];
for await (const range of matchAll(corpus)) {
ranges.push(range);
}
for (const range of ranges) {
highlight(range);
}
parsed.innerText = JSON.stringify(selector, null, 2);
};
async function describeSelection() {
const selection = document.getSelection();
if (selection.type !== 'Range') return;
const range = selection.getRangeAt(0);
if (range.collapsed) return;
const scope = document.createRange();
scope.selectNodeContents(selectable);
if (!scope.isPointInRange(range.startContainer, range.startOffset)) return;
if (!scope.isPointInRange(range.endContainer, range.endOffset)) return;
return describeTextQuote(range, scope);
}
async function onSelectionChange() {
const selector = await describeSelection();
const fragment = selector ? stringifyFragment(selector) : '';
const url = new URL(window.location.href);
url.hash = fragment ? `#${fragment}` : '';
if (url.href !== window.location.href) {
window.history.replaceState(selector, null, url.href);
refresh();
}
}
window.addEventListener('popstate', refresh);
document.addEventListener('DOMContentLoaded', refresh);
document.addEventListener('selectionchange', onSelectionChange);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => {
window.removeEventListener('popstate', refresh);
document.removeEventListener('DOMContentLoaded', refresh);
document.removeEventListener('selectionchange', onSelectionChange);
});
}