Skip to content

Instantly share code, notes, and snippets.

@eligrey
Last active January 9, 2025 01:23
Show Gist options
  • Save eligrey/c18267a9a9fa1d98226b133c22167c91 to your computer and use it in GitHub Desktop.
Save eligrey/c18267a9a9fa1d98226b133c22167c91 to your computer and use it in GitHub Desktop.

Revisions

  1. eligrey revised this gist Dec 4, 2024. 1 changed file with 10 additions and 17 deletions.
    27 changes: 10 additions & 17 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    // ==UserScript==
    // @name Persist text input
    // @description Persists input in text fields between navigations
    // @author Eli Grey
    // @author Eli Grey, The Chromium Authors
    // @namespace https://eligrey.com
    // @version 1.0.0
    // @match *://*/*
    @@ -17,39 +17,29 @@
    2. Click the 'Raw' button at the top of this text to install the userscript
    */

    /*! MIT License
    /*!
    MIT License
    Copyright (c) 2024 Eli Grey
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    */

    // CONFIGURATION
    @@ -142,9 +132,12 @@ if (MUTATION_OBSERVER_CUTOFF !== -1) {
    // utils from https://stackoverflow.com/a/58677712/78436
    // and https://github.com/chromium/chromium/blob/77578ccb4082ae20a9326d9e673225f1189ebb63/third_party/blink/renderer/devtools/front_end/elements/DOMPath.js#L242

    // Copyright 2018 The Chromium Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style license that can be
    // found in the LICENSE file.
    /*!
    * Copyright 2018 The Chromium Authors. All rights reserved.
    * Use of this source code is governed by a BSD-style license that can be
    * found in the LICENSE file at
    * https://chromium.googlesource.com/devtools/devtools-frontend/+/29905850a9777d36fbca9900cd306003c7bfe824/LICENSE
    /
    const Elements = {};
    Elements.DOMPath = {};
  2. eligrey revised this gist Dec 1, 2024. 1 changed file with 6 additions and 6 deletions.
    12 changes: 6 additions & 6 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -11,6 +11,12 @@
    // @license MIT
    // ==/UserScript==

    /*
    How to install:
    1. Install the Tampermonkey extension for your browser
    2. Click the 'Raw' button at the top of this text to install the userscript
    */

    /*! MIT License
    Copyright (c) 2024 Eli Grey
    @@ -46,12 +52,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    */

    /*
    How to install:
    1. Install the Tampermonkey extension for your browser
    2. Click the 'Raw' button at the top of this text to install the userscript
    */

    // CONFIGURATION

    // Time in ms after the document is loaded to stop attempting to resolve & restore input after
  3. eligrey revised this gist Dec 1, 2024. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -8,13 +8,12 @@
    // @grant none
    // @run-at document-end
    // @charset UTF-8
    // @copyright 2024, Transcend, Inc.
    // @license MIT
    // ==/UserScript==

    /*! MIT License
    Copyright (c) 2024 Transcend Inc.
    Copyright (c) 2024 Eli Grey
    Permission is hereby granted, free of charge, to any person obtaining a copy
  4. eligrey revised this gist Dec 1, 2024. 1 changed file with 35 additions and 0 deletions.
    35 changes: 35 additions & 0 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -12,6 +12,41 @@
    // @license MIT
    // ==/UserScript==

    /*! MIT License
    Copyright (c) 2024 Transcend Inc.
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    */

    /*
    How to install:
    1. Install the Tampermonkey extension for your browser
  5. eligrey revised this gist Dec 1, 2024. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -8,6 +8,8 @@
    // @grant none
    // @run-at document-end
    // @charset UTF-8
    // @copyright 2024, Transcend, Inc.
    // @license MIT
    // ==/UserScript==

    /*
  6. eligrey revised this gist Sep 10, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -22,7 +22,7 @@
    // DOM mutations. Set to -1 to always keep mutation observer active, which has a performance cost.
    const MUTATION_OBSERVER_CUTOFF = -1;

    // Reduces XPath specificity when enabled.
    // Reduces specificity of generated XPath selectors when enabled.
    const REDUCED_XPATH_SPECIFICITY = false;

    // END CONFIGURATION
  7. eligrey revised this gist Sep 10, 2024. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -21,6 +21,7 @@
    // Time in ms after the document is loaded to stop attempting to resolve & restore input after
    // DOM mutations. Set to -1 to always keep mutation observer active, which has a performance cost.
    const MUTATION_OBSERVER_CUTOFF = -1;

    // Reduces XPath specificity when enabled.
    const REDUCED_XPATH_SPECIFICITY = false;

  8. eligrey revised this gist Sep 10, 2024. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -21,8 +21,8 @@
    // Time in ms after the document is loaded to stop attempting to resolve & restore input after
    // DOM mutations. Set to -1 to always keep mutation observer active, which has a performance cost.
    const MUTATION_OBSERVER_CUTOFF = -1;
    // Optimize XPath expression generation. Reduces XPath specificity when enabled.
    const OPTIMIZE_XPATH_GENERATION = false;
    // Reduces XPath specificity when enabled.
    const REDUCED_XPATH_SPECIFICITY = false;

    // END CONFIGURATION

    @@ -81,7 +81,7 @@ addEventListener('pagehide', () => {
    const key = getInputMapsKey();
    const index = inputMaps.findIndex(([page]) => page === key);
    const serialized = [key, [...inputMap].map(
    ([target, value]) => [Elements.DOMPath.xPath(target, OPTIMIZE_XPATH_GENERATION), value]
    ([target, value]) => [Elements.DOMPath.xPath(target, REDUCED_XPATH_SPECIFICITY), value]
    )];
    const found = index !== -1;
    inputMaps.splice(found ? index : inputMaps.length, found, serialized);
  9. eligrey revised this gist Sep 9, 2024. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@
    // @description Persists input in text fields between navigations
    // @author Eli Grey
    // @namespace https://eligrey.com
    // @version 2024-09-09
    // @version 1.0.0
    // @match *://*/*
    // @grant none
    // @run-at document-end
    @@ -69,7 +69,7 @@ addEventListener('input', ({ target }) => {
    updateInputMap();
    }
    if (target instanceof Element) {
    inputMap.set(target, target.value || target.textContent);
    inputMap.set(target, target.value);
    }
    })

  10. eligrey revised this gist Sep 9, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -26,7 +26,7 @@ const OPTIMIZE_XPATH_GENERATION = false;

    // END CONFIGURATION

    const getInputMapsKey = () => `${location.pathname}${location.search}`;
    const getInputMapsKey = () => location.pathname + location.search;

    // restore input
    let inputMaps;
  11. eligrey revised this gist Sep 9, 2024. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -26,14 +26,14 @@ const OPTIMIZE_XPATH_GENERATION = false;

    // END CONFIGURATION

    const getInputMapKey = () => `${location.pathname}${location.search}`;
    const getInputMapsKey = () => `${location.pathname}${location.search}`;

    // restore input
    let inputMaps;
    const resolveInputMap = () => {
    const { inputCache } = sessionStorage;
    inputMaps = inputCache ? JSON.parse(inputCache) : [];
    const key = getInputMapKey();
    const key = getInputMapsKey();
    return new Map(
    (inputMaps.find(([page]) => page === key)?.[1] || []).map(([xpath, value]) =>
    [document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue, value]
    @@ -78,7 +78,7 @@ addEventListener('pagehide', () => {
    if (!inputMap) {
    updateInputMap();
    }
    const key = getInputMapKey();
    const key = getInputMapsKey();
    const index = inputMaps.findIndex(([page]) => page === key);
    const serialized = [key, [...inputMap].map(
    ([target, value]) => [Elements.DOMPath.xPath(target, OPTIMIZE_XPATH_GENERATION), value]
  12. eligrey revised this gist Sep 9, 2024. No changes.
  13. eligrey revised this gist Sep 9, 2024. 1 changed file with 7 additions and 3 deletions.
    10 changes: 7 additions & 3 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -26,13 +26,16 @@ const OPTIMIZE_XPATH_GENERATION = false;

    // END CONFIGURATION

    const getInputMapKey = () => `${location.pathname}${location.search}`;

    // restore input
    let inputMaps;
    const resolveInputMap = () => {
    const { inputCache } = sessionStorage;
    inputMaps = inputCache ? JSON.parse(inputCache) : [];
    const key = getInputMapKey();
    return new Map(
    (inputMaps.find(([page]) => page === location.pathname)?.[1] || []).map(([xpath, value]) =>
    (inputMaps.find(([page]) => page === key)?.[1] || []).map(([xpath, value]) =>
    [document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue, value]
    )
    );
    @@ -75,8 +78,9 @@ addEventListener('pagehide', () => {
    if (!inputMap) {
    updateInputMap();
    }
    const index = inputMaps.findIndex(([page]) => page === location.pathname);
    const serialized = [location.pathname, [...inputMap].map(
    const key = getInputMapKey();
    const index = inputMaps.findIndex(([page]) => page === key);
    const serialized = [key, [...inputMap].map(
    ([target, value]) => [Elements.DOMPath.xPath(target, OPTIMIZE_XPATH_GENERATION), value]
    )];
    const found = index !== -1;
  14. eligrey revised this gist Sep 9, 2024. No changes.
  15. eligrey revised this gist Sep 9, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -77,7 +77,7 @@ addEventListener('pagehide', () => {
    }
    const index = inputMaps.findIndex(([page]) => page === location.pathname);
    const serialized = [location.pathname, [...inputMap].map(
    ([target, value]) => [Elements.DOMPath.xPath(target, USE_OPTIMIZED_XPATH_QUERIES), value]
    ([target, value]) => [Elements.DOMPath.xPath(target, OPTIMIZE_XPATH_GENERATION), value]
    )];
    const found = index !== -1;
    inputMaps.splice(found ? index : inputMaps.length, found, serialized);
  16. eligrey revised this gist Sep 9, 2024. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -21,6 +21,8 @@
    // Time in ms after the document is loaded to stop attempting to resolve & restore input after
    // DOM mutations. Set to -1 to always keep mutation observer active, which has a performance cost.
    const MUTATION_OBSERVER_CUTOFF = -1;
    // Optimize XPath expression generation. Reduces XPath specificity when enabled.
    const OPTIMIZE_XPATH_GENERATION = false;

    // END CONFIGURATION

    @@ -75,7 +77,7 @@ addEventListener('pagehide', () => {
    }
    const index = inputMaps.findIndex(([page]) => page === location.pathname);
    const serialized = [location.pathname, [...inputMap].map(
    ([target, value]) => [Elements.DOMPath.xPath(target, false), value]
    ([target, value]) => [Elements.DOMPath.xPath(target, USE_OPTIMIZED_XPATH_QUERIES), value]
    )];
    const found = index !== -1;
    inputMaps.splice(found ? index : inputMaps.length, found, serialized);
  17. eligrey revised this gist Sep 9, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -13,7 +13,7 @@
    /*
    How to install:
    1. Install the Tampermonkey extension for your browser
    2. Click the 'raw' button at the top of this text to install the userscript
    2. Click the 'Raw' button at the top of this text to install the userscript
    */

    // CONFIGURATION
  18. eligrey revised this gist Sep 9, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -20,7 +20,7 @@

    // Time in ms after the document is loaded to stop attempting to resolve & restore input after
    // DOM mutations. Set to -1 to always keep mutation observer active, which has a performance cost.
    const MUTATION_OBSERVER_CUTOFF = 20000; // default is 20 seconds
    const MUTATION_OBSERVER_CUTOFF = -1;

    // END CONFIGURATION

  19. eligrey revised this gist Sep 9, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -20,7 +20,7 @@

    // Time in ms after the document is loaded to stop attempting to resolve & restore input after
    // DOM mutations. Set to -1 to always keep mutation observer active, which has a performance cost.
    const MUTATION_OBSERVER_CUTOFF = 10000; // default is 10 seconds
    const MUTATION_OBSERVER_CUTOFF = 20000; // default is 20 seconds

    // END CONFIGURATION

  20. eligrey revised this gist Sep 9, 2024. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -47,8 +47,8 @@ const restoreInput = () => {
    }
    }
    };
    const mutationObserver = new MutationObserver(({ type, addedNodes }) => {
    if (type === 'childList' && addedNodes.length !== 0) {
    const mutationObserver = new MutationObserver((mutations) => {
    if (mutations.some(({ type, addedNodes }) => type === 'childList' && addedNodes.length !== 0)) {
    restoreInput();
    }
    });
  21. eligrey revised this gist Sep 9, 2024. 1 changed file with 25 additions and 2 deletions.
    27 changes: 25 additions & 2 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -16,6 +16,14 @@
    2. Click the 'raw' button at the top of this text to install the userscript
    */

    // CONFIGURATION

    // Time in ms after the document is loaded to stop attempting to resolve & restore input after
    // DOM mutations. Set to -1 to always keep mutation observer active, which has a performance cost.
    const MUTATION_OBSERVER_CUTOFF = 10000; // default is 10 seconds

    // END CONFIGURATION

    // restore input
    let inputMaps;
    const resolveInputMap = () => {
    @@ -39,11 +47,12 @@ const restoreInput = () => {
    }
    }
    };
    new MutationObserver(({ type, addedNodes }) => {
    const mutationObserver = new MutationObserver(({ type, addedNodes }) => {
    if (type === 'childList' && addedNodes.length !== 0) {
    restoreInput();
    }
    }).observe(document, {
    });
    mutationObserver.observe(document, {
    subtree: true,
    childList: true,
    });
    @@ -73,6 +82,20 @@ addEventListener('pagehide', () => {
    sessionStorage.inputCache = JSON.stringify(inputMaps);
    });

    // auto-remove mutation observer
    if (MUTATION_OBSERVER_CUTOFF !== -1) {
    const scheduleObserverRemoval = () => {
    setTimeout(() => {
    mutationObserver.disconnect();
    }, MUTATION_OBSERVER_CUTOFF);
    }
    if (document.readyState === 'complete') {
    scheduleObserverRemoval();
    } else {
    addEventListener('load', scheduleObserverRemoval);
    }
    }

    // utils from https://stackoverflow.com/a/58677712/78436
    // and https://github.com/chromium/chromium/blob/77578ccb4082ae20a9326d9e673225f1189ebb63/third_party/blink/renderer/devtools/front_end/elements/DOMPath.js#L242

  22. eligrey revised this gist Sep 9, 2024. 1 changed file with 2 additions and 3 deletions.
    5 changes: 2 additions & 3 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -60,7 +60,7 @@ addEventListener('input', ({ target }) => {
    })

    // persist input
    const persistInput = () => {
    addEventListener('pagehide', () => {
    if (!inputMap) {
    updateInputMap();
    }
    @@ -71,8 +71,7 @@ const persistInput = () => {
    const found = index !== -1;
    inputMaps.splice(found ? index : inputMaps.length, found, serialized);
    sessionStorage.inputCache = JSON.stringify(inputMaps);
    };
    addEventListener('pagehide', persistInput);
    });

    // utils from https://stackoverflow.com/a/58677712/78436
    // and https://github.com/chromium/chromium/blob/77578ccb4082ae20a9326d9e673225f1189ebb63/third_party/blink/renderer/devtools/front_end/elements/DOMPath.js#L242
  23. eligrey revised this gist Sep 9, 2024. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -12,8 +12,8 @@

    /*
    How to install:
    - Install the Tampermonkey extension for your browser
    - Click the 'raw' button at the top of this text to install the userscript
    1. Install the Tampermonkey extension for your browser
    2. Click the 'raw' button at the top of this text to install the userscript
    */

    // restore input
  24. eligrey revised this gist Sep 9, 2024. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -10,6 +10,12 @@
    // @charset UTF-8
    // ==/UserScript==

    /*
    How to install:
    - Install the Tampermonkey extension for your browser
    - Click the 'raw' button at the top of this text to install the userscript
    */

    // restore input
    let inputMaps;
    const resolveInputMap = () => {
  25. eligrey revised this gist Sep 9, 2024. No changes.
  26. eligrey revised this gist Sep 9, 2024. 1 changed file with 121 additions and 116 deletions.
    237 changes: 121 additions & 116 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -2,11 +2,11 @@
    // @name Persist text input
    // @description Persists input in text fields between navigations
    // @author Eli Grey
    // @namespace https://dangerous.link/virus.exe
    // @version 2024-09-08
    // @namespace https://eligrey.com
    // @version 2024-09-09
    // @match *://*/*
    // @grant none
    // @run-at document-start
    // @run-at document-end
    // @charset UTF-8
    // ==/UserScript==

    @@ -24,19 +24,24 @@ const resolveInputMap = () => {
    let inputMap;
    const updateInputMap = () => {
    inputMap = resolveInputMap();
    }
    };
    const restoreInput = () => {
    updateInputMap();
    for (const [target, value] of inputMap) {
    if (target) {
    target.value = value;
    }
    }
    }
    new MutationObserver(restoreInput).observe(document, {
    };
    new MutationObserver(({ type, addedNodes }) => {
    if (type === 'childList' && addedNodes.length !== 0) {
    restoreInput();
    }
    }).observe(document, {
    subtree: true,
    childList: true,
    })
    });
    restoreInput();

    // track input
    addEventListener('input', ({ target }) => {
    @@ -79,26 +84,26 @@ Elements.DOMPath = {};
    * @return {string}
    */
    Elements.DOMPath.xPath = function (node, optimized) {
    if (node.nodeType === Node.DOCUMENT_NODE) {
    return '/';
    }
    if (node.nodeType === Node.DOCUMENT_NODE) {
    return '/';
    }

    const steps = [];
    let contextNode = node;
    while (contextNode) {
    const step = Elements.DOMPath._xPathValue(contextNode, optimized);
    if (!step) {
    break;
    } // Error - bail out early.
    steps.push(step);
    if (step.optimized) {
    break;
    }
    contextNode = contextNode.parentNode;
    const steps = [];
    let contextNode = node;
    while (contextNode) {
    const step = Elements.DOMPath._xPathValue(contextNode, optimized);
    if (!step) {
    break;
    } // Error - bail out early.
    steps.push(step);
    if (step.optimized) {
    break;
    }
    contextNode = contextNode.parentNode;
    }

    steps.reverse();
    return (steps.length && steps[0].optimized ? '' : '/') + steps.join('/');
    steps.reverse();
    return (steps.length && steps[0].optimized ? '' : '/') + steps.join('/');
    };

    /**
    @@ -107,117 +112,117 @@ Elements.DOMPath.xPath = function (node, optimized) {
    * @return {?Elements.DOMPath.Step}
    */
    Elements.DOMPath._xPathValue = function (node, optimized) {
    let ownValue;
    const ownIndex = Elements.DOMPath._xPathIndex(node);
    if (ownIndex === -1) {
    return null;
    } // Error.

    switch (node.nodeType) {
    case Node.ELEMENT_NODE:
    if (optimized && node.getAttribute('id')) {
    return new Elements.DOMPath.Step('//*[@id="' + node.getAttribute('id') + '"]', true);
    }
    ownValue = node.localName;
    break;
    case Node.ATTRIBUTE_NODE:
    ownValue = '@' + node.nodeName;
    break;
    case Node.TEXT_NODE:
    case Node.CDATA_SECTION_NODE:
    ownValue = 'text()';
    break;
    case Node.PROCESSING_INSTRUCTION_NODE:
    ownValue = 'processing-instruction()';
    break;
    case Node.COMMENT_NODE:
    ownValue = 'comment()';
    break;
    case Node.DOCUMENT_NODE:
    ownValue = '';
    break;
    default:
    ownValue = '';
    break;
    }
    let ownValue;
    const ownIndex = Elements.DOMPath._xPathIndex(node);
    if (ownIndex === -1) {
    return null;
    } // Error.

    switch (node.nodeType) {
    case Node.ELEMENT_NODE:
    if (optimized && node.getAttribute('id')) {
    return new Elements.DOMPath.Step('//*[@id="' + node.getAttribute('id') + '"]', true);
    }
    ownValue = node.localName;
    break;
    case Node.ATTRIBUTE_NODE:
    ownValue = '@' + node.nodeName;
    break;
    case Node.TEXT_NODE:
    case Node.CDATA_SECTION_NODE:
    ownValue = 'text()';
    break;
    case Node.PROCESSING_INSTRUCTION_NODE:
    ownValue = 'processing-instruction()';
    break;
    case Node.COMMENT_NODE:
    ownValue = 'comment()';
    break;
    case Node.DOCUMENT_NODE:
    ownValue = '';
    break;
    default:
    ownValue = '';
    break;
    }

    if (ownIndex > 0) {
    ownValue += '[' + ownIndex + ']';
    }
    if (ownIndex > 0) {
    ownValue += '[' + ownIndex + ']';
    }

    return new Elements.DOMPath.Step(ownValue, node.nodeType === Node.DOCUMENT_NODE);
    return new Elements.DOMPath.Step(ownValue, node.nodeType === Node.DOCUMENT_NODE);
    };

    /**
    * @param {!Node} node
    * @return {number}
    */
    Elements.DOMPath._xPathIndex = function (node) {
    // Returns -1 in case of error, 0 if no siblings matching the same expression,
    // <XPath index among the same expression-matching sibling nodes> otherwise.
    function areNodesSimilar(left, right) {
    if (left === right) {
    return true;
    }

    if (left.nodeType === Node.ELEMENT_NODE && right.nodeType === Node.ELEMENT_NODE) {
    return left.localName === right.localName;
    }

    if (left.nodeType === right.nodeType) {
    return true;
    }

    // XPath treats CDATA as text nodes.
    const leftType = left.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType;
    const rightType = right.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType;
    return leftType === rightType;
    // Returns -1 in case of error, 0 if no siblings matching the same expression,
    // <XPath index among the same expression-matching sibling nodes> otherwise.
    function areNodesSimilar(left, right) {
    if (left === right) {
    return true;
    }

    const siblings = node.parentNode ? node.parentNode.children : null;
    if (!siblings) {
    return 0;
    } // Root node - no siblings.
    let hasSameNamedElements;
    for (let i = 0; i < siblings.length; ++i) {
    if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
    hasSameNamedElements = true;
    break;
    }
    if (left.nodeType === Node.ELEMENT_NODE && right.nodeType === Node.ELEMENT_NODE) {
    return left.localName === right.localName;
    }
    if (!hasSameNamedElements) {
    return 0;

    if (left.nodeType === right.nodeType) {
    return true;
    }

    // XPath treats CDATA as text nodes.
    const leftType = left.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType;
    const rightType = right.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType;
    return leftType === rightType;
    }

    const siblings = node.parentNode ? node.parentNode.children : null;
    if (!siblings) {
    return 0;
    } // Root node - no siblings.
    let hasSameNamedElements;
    for (let i = 0; i < siblings.length; ++i) {
    if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
    hasSameNamedElements = true;
    break;
    }
    let ownIndex = 1; // XPath indices start with 1.
    for (let i = 0; i < siblings.length; ++i) {
    if (areNodesSimilar(node, siblings[i])) {
    if (siblings[i] === node) {
    return ownIndex;
    }
    ++ownIndex;
    }
    }
    if (!hasSameNamedElements) {
    return 0;
    }
    let ownIndex = 1; // XPath indices start with 1.
    for (let i = 0; i < siblings.length; ++i) {
    if (areNodesSimilar(node, siblings[i])) {
    if (siblings[i] === node) {
    return ownIndex;
    }
    ++ownIndex;
    }
    return -1; // An error occurred: |node| not found in parent's children.
    }
    return -1; // An error occurred: |node| not found in parent's children.
    };

    /**
    * @unrestricted
    */
    Elements.DOMPath.Step = class {
    /**
    * @param {string} value
    * @param {boolean} optimized
    */
    constructor(value, optimized) {
    this.value = value;
    this.optimized = optimized || false;
    }
    /**
    * @param {string} value
    * @param {boolean} optimized
    */
    constructor(value, optimized) {
    this.value = value;
    this.optimized = optimized || false;
    }

    /**
    * @override
    * @return {string}
    */
    toString() {
    return this.value;
    }
    /**
    * @override
    * @return {string}
    */
    toString() {
    return this.value;
    }
    };
  27. eligrey created this gist Sep 9, 2024.
    223 changes: 223 additions & 0 deletions persist-text-input.user.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,223 @@
    // ==UserScript==
    // @name Persist text input
    // @description Persists input in text fields between navigations
    // @author Eli Grey
    // @namespace https://dangerous.link/virus.exe
    // @version 2024-09-08
    // @match *://*/*
    // @grant none
    // @run-at document-start
    // @charset UTF-8
    // ==/UserScript==

    // restore input
    let inputMaps;
    const resolveInputMap = () => {
    const { inputCache } = sessionStorage;
    inputMaps = inputCache ? JSON.parse(inputCache) : [];
    return new Map(
    (inputMaps.find(([page]) => page === location.pathname)?.[1] || []).map(([xpath, value]) =>
    [document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue, value]
    )
    );
    }
    let inputMap;
    const updateInputMap = () => {
    inputMap = resolveInputMap();
    }
    const restoreInput = () => {
    updateInputMap();
    for (const [target, value] of inputMap) {
    if (target) {
    target.value = value;
    }
    }
    }
    new MutationObserver(restoreInput).observe(document, {
    subtree: true,
    childList: true,
    })

    // track input
    addEventListener('input', ({ target }) => {
    if (!inputMap) {
    updateInputMap();
    }
    if (target instanceof Element) {
    inputMap.set(target, target.value || target.textContent);
    }
    })

    // persist input
    const persistInput = () => {
    if (!inputMap) {
    updateInputMap();
    }
    const index = inputMaps.findIndex(([page]) => page === location.pathname);
    const serialized = [location.pathname, [...inputMap].map(
    ([target, value]) => [Elements.DOMPath.xPath(target, false), value]
    )];
    const found = index !== -1;
    inputMaps.splice(found ? index : inputMaps.length, found, serialized);
    sessionStorage.inputCache = JSON.stringify(inputMaps);
    };
    addEventListener('pagehide', persistInput);

    // utils from https://stackoverflow.com/a/58677712/78436
    // and https://github.com/chromium/chromium/blob/77578ccb4082ae20a9326d9e673225f1189ebb63/third_party/blink/renderer/devtools/front_end/elements/DOMPath.js#L242

    // Copyright 2018 The Chromium Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style license that can be
    // found in the LICENSE file.

    const Elements = {};
    Elements.DOMPath = {};

    /**
    * @param {!Node} node
    * @param {boolean=} optimized
    * @return {string}
    */
    Elements.DOMPath.xPath = function (node, optimized) {
    if (node.nodeType === Node.DOCUMENT_NODE) {
    return '/';
    }

    const steps = [];
    let contextNode = node;
    while (contextNode) {
    const step = Elements.DOMPath._xPathValue(contextNode, optimized);
    if (!step) {
    break;
    } // Error - bail out early.
    steps.push(step);
    if (step.optimized) {
    break;
    }
    contextNode = contextNode.parentNode;
    }

    steps.reverse();
    return (steps.length && steps[0].optimized ? '' : '/') + steps.join('/');
    };

    /**
    * @param {!Node} node
    * @param {boolean=} optimized
    * @return {?Elements.DOMPath.Step}
    */
    Elements.DOMPath._xPathValue = function (node, optimized) {
    let ownValue;
    const ownIndex = Elements.DOMPath._xPathIndex(node);
    if (ownIndex === -1) {
    return null;
    } // Error.

    switch (node.nodeType) {
    case Node.ELEMENT_NODE:
    if (optimized && node.getAttribute('id')) {
    return new Elements.DOMPath.Step('//*[@id="' + node.getAttribute('id') + '"]', true);
    }
    ownValue = node.localName;
    break;
    case Node.ATTRIBUTE_NODE:
    ownValue = '@' + node.nodeName;
    break;
    case Node.TEXT_NODE:
    case Node.CDATA_SECTION_NODE:
    ownValue = 'text()';
    break;
    case Node.PROCESSING_INSTRUCTION_NODE:
    ownValue = 'processing-instruction()';
    break;
    case Node.COMMENT_NODE:
    ownValue = 'comment()';
    break;
    case Node.DOCUMENT_NODE:
    ownValue = '';
    break;
    default:
    ownValue = '';
    break;
    }

    if (ownIndex > 0) {
    ownValue += '[' + ownIndex + ']';
    }

    return new Elements.DOMPath.Step(ownValue, node.nodeType === Node.DOCUMENT_NODE);
    };

    /**
    * @param {!Node} node
    * @return {number}
    */
    Elements.DOMPath._xPathIndex = function (node) {
    // Returns -1 in case of error, 0 if no siblings matching the same expression,
    // <XPath index among the same expression-matching sibling nodes> otherwise.
    function areNodesSimilar(left, right) {
    if (left === right) {
    return true;
    }

    if (left.nodeType === Node.ELEMENT_NODE && right.nodeType === Node.ELEMENT_NODE) {
    return left.localName === right.localName;
    }

    if (left.nodeType === right.nodeType) {
    return true;
    }

    // XPath treats CDATA as text nodes.
    const leftType = left.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType;
    const rightType = right.nodeType === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType;
    return leftType === rightType;
    }

    const siblings = node.parentNode ? node.parentNode.children : null;
    if (!siblings) {
    return 0;
    } // Root node - no siblings.
    let hasSameNamedElements;
    for (let i = 0; i < siblings.length; ++i) {
    if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
    hasSameNamedElements = true;
    break;
    }
    }
    if (!hasSameNamedElements) {
    return 0;
    }
    let ownIndex = 1; // XPath indices start with 1.
    for (let i = 0; i < siblings.length; ++i) {
    if (areNodesSimilar(node, siblings[i])) {
    if (siblings[i] === node) {
    return ownIndex;
    }
    ++ownIndex;
    }
    }
    return -1; // An error occurred: |node| not found in parent's children.
    };

    /**
    * @unrestricted
    */
    Elements.DOMPath.Step = class {
    /**
    * @param {string} value
    * @param {boolean} optimized
    */
    constructor(value, optimized) {
    this.value = value;
    this.optimized = optimized || false;
    }

    /**
    * @override
    * @return {string}
    */
    toString() {
    return this.value;
    }
    };