mirror of
https://github.com/jakejarvis/simple-anchor.git
synced 2025-04-25 17:35:21 -04:00
initial commit 🎉
This commit is contained in:
commit
057a3f87b1
17
.eslintrc.json
Normal file
17
.eslintrc.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": [
|
||||
"@jakejarvis/eslint-config"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2015,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"dist/**",
|
||||
"rollup.config.js"
|
||||
]
|
||||
}
|
15
.github/workflows/ci.yml
vendored
Normal file
15
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
.npmrc
|
||||
.vscode/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Bryan Braun
|
||||
|
||||
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.
|
53
README.md
Normal file
53
README.md
Normal file
@ -0,0 +1,53 @@
|
||||
# 🔗 Simple Anchor [](https://www.npmjs.com/package/simple-anchor)
|
||||
|
||||
A JavaScript utility for adding deep anchor links ([like these](https://ux.stackexchange.com/q/36304/33248)) to existing page content.
|
||||
|
||||
## Changes from [AnchorJS](https://github.com/bryanbraun/anchorjs)
|
||||
|
||||
- Styling of `.anchorjs-link` elements is completely on you. The non-optional and hefty [base styles](https://github.com/bryanbraun/anchorjs/blob/7a2e93892fc8c1eeba0a9de5025feabf79372158/anchor.js#L305) of AnchorJS have been removed for a slimmer module. (This includes the default 🔗 icon.)
|
||||
- Element IDs are also left to you — this package will **not** generate an element's ID automatically if one is not already set (eg. `<h2 id="installation">Installation</h2>`). Elements without one are automatically ignored.
|
||||
|
||||
Otherwise, the [AnchorJS docs](https://www.bryanbraun.com/anchorjs/) still serve as a good reference.
|
||||
|
||||
## Usage
|
||||
|
||||
### Browser
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/@jakejarvis/simple-anchor/dist/simple-anchor.min.js"></script>
|
||||
<script>
|
||||
var anchor = new SimpleAnchor();
|
||||
anchors.add({
|
||||
icon: '#'
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```bash
|
||||
npm install simple-anchor
|
||||
# or...
|
||||
yarn add simple-anchor
|
||||
```
|
||||
|
||||
```js
|
||||
import SimpleAnchor from 'simple-anchor';
|
||||
// or...
|
||||
// const SimpleAnchor = require('simple-anchor');
|
||||
|
||||
const anchors = new SimpleAnchor();
|
||||
anchors.add({
|
||||
icon: '#'
|
||||
});
|
||||
```
|
||||
|
||||
Since AnchorJS's default CSS has been removed as mentioned above, it's up to you to style the `.anchorjs-link` element.
|
||||
|
||||
## Examples
|
||||
|
||||
- [https://jarv.is/](https://jarv.is/) ([Source](https://github.com/jakejarvis/jarv.is))
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
59
package.json
Normal file
59
package.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "simple-anchor",
|
||||
"version": "1.0.0",
|
||||
"author": "Jake Jarvis (https://github.com/jakejarvis)",
|
||||
"contributors": [
|
||||
"Bryan Braun (https://github.com/bryanbraun)"
|
||||
],
|
||||
"description": "A bare-bones fork of AnchorJS.",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/jakejarvis/simple-anchor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jakejarvis/simple-anchor.git"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/simple-anchor.cjs.js",
|
||||
"module": "./dist/simple-anchor.esm.js",
|
||||
"unpkg": "./dist/simple-anchor.min.js",
|
||||
"types": "./dist/simple-anchor.d.ts",
|
||||
"exports": {
|
||||
"require": "./dist/simple-anchor.cjs.js",
|
||||
"import": "./dist/simple-anchor.esm.js",
|
||||
"browser": "./dist/simple-anchor.min.js"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "rollup -c",
|
||||
"lint": "eslint .",
|
||||
"prepublishOnly": "run-s clean build"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@jakejarvis/eslint-config": "^1.0.1",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-eslint": "^8.0.1",
|
||||
"@rollup/plugin-node-resolve": "^13.0.4",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-compat": "^3.13.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.56.3",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-filesize": "^9.1.1",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
},
|
||||
"keywords": [
|
||||
"frontend",
|
||||
"anchor",
|
||||
"links",
|
||||
"dom",
|
||||
"browser"
|
||||
]
|
||||
}
|
89
rollup.config.js
Normal file
89
rollup.config.js
Normal file
@ -0,0 +1,89 @@
|
||||
import pkg from "./package.json";
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import { babel } from "@rollup/plugin-babel";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import eslint from "@rollup/plugin-eslint";
|
||||
import filesize from "rollup-plugin-filesize";
|
||||
import copy from "rollup-plugin-copy";
|
||||
|
||||
const banner = `/*! Simple Anchor v${pkg.version} | MIT License | https://github.com/jakejarvis/simple-anchor */`;
|
||||
|
||||
export default [
|
||||
{
|
||||
// universal (browser and node)
|
||||
input: "src/index.js",
|
||||
output: [
|
||||
{
|
||||
name: "SimpleAnchor",
|
||||
file: "dist/simple-anchor.js",
|
||||
format: "umd",
|
||||
exports: "named",
|
||||
esModule: false,
|
||||
banner: banner,
|
||||
},
|
||||
{
|
||||
name: "SimpleAnchor",
|
||||
file: "dist/simple-anchor.min.js",
|
||||
format: "umd",
|
||||
exports: "named",
|
||||
esModule: false,
|
||||
plugins: [
|
||||
terser({
|
||||
format: {
|
||||
preamble: banner,
|
||||
ascii_only: true, // default icon symbol gets disfigured otherwise
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
copy({
|
||||
// clearly this isn't really typescript, so we need to manually copy the type definition file
|
||||
targets: [
|
||||
{
|
||||
src: "src/index.d.ts",
|
||||
dest: "dist",
|
||||
rename: "simple-anchor.d.ts",
|
||||
},
|
||||
],
|
||||
}),
|
||||
resolve(),
|
||||
eslint(),
|
||||
babel({
|
||||
babelHelpers: "bundled",
|
||||
presets: [["@babel/preset-env"]],
|
||||
exclude: ["node_modules/**"],
|
||||
}),
|
||||
filesize(),
|
||||
],
|
||||
},
|
||||
{
|
||||
// modules
|
||||
input: "src/index.js",
|
||||
output: [
|
||||
{
|
||||
// ES6 module (import)
|
||||
file: "dist/simple-anchor.esm.js",
|
||||
format: "esm",
|
||||
exports: "named",
|
||||
banner: banner,
|
||||
},
|
||||
{
|
||||
// commonjs (require)
|
||||
file: "dist/simple-anchor.cjs.js",
|
||||
format: "cjs",
|
||||
exports: "named",
|
||||
banner: banner,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
resolve(),
|
||||
babel({
|
||||
babelHelpers: "bundled",
|
||||
exclude: ["node_modules/**"],
|
||||
}),
|
||||
filesize(),
|
||||
],
|
||||
},
|
||||
];
|
39
src/index.d.ts
vendored
Normal file
39
src/index.d.ts
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
// Adapted from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/anchor-js/index.d.ts
|
||||
|
||||
declare namespace simpleAnchor {
|
||||
interface Anchor {
|
||||
options: AnchorOptions;
|
||||
|
||||
add(selector?: string): Anchor;
|
||||
remove(selector?: string): Anchor;
|
||||
removeAll(): void;
|
||||
}
|
||||
|
||||
type AnchorPlacement = 'left' | 'right';
|
||||
type AnchorVisibility = 'always' | 'hover' | 'touch';
|
||||
|
||||
interface AnchorOptions {
|
||||
ariaLabel?: string | undefined;
|
||||
base?: string | undefined;
|
||||
class?: string | undefined;
|
||||
icon?: string | undefined;
|
||||
placement?: AnchorPlacement | undefined;
|
||||
titleText?: string | undefined;
|
||||
visible?: AnchorVisibility | undefined;
|
||||
}
|
||||
|
||||
interface AnchorStatic {
|
||||
new(options?: AnchorOptions): Anchor;
|
||||
}
|
||||
}
|
||||
|
||||
declare const anchors: simpleAnchor.Anchor;
|
||||
declare const SimpleAnchor: simpleAnchor.AnchorStatic;
|
||||
|
||||
export = SimpleAnchor;
|
||||
|
||||
export as namespace SimpleAnchor;
|
||||
|
||||
declare global {
|
||||
const anchors: simpleAnchor.Anchor;
|
||||
}
|
194
src/index.js
Normal file
194
src/index.js
Normal file
@ -0,0 +1,194 @@
|
||||
"use strict";
|
||||
|
||||
const SimpleAnchor = function (options) {
|
||||
this.options = options || {};
|
||||
this.elements = [];
|
||||
|
||||
/**
|
||||
* Assigns options to the internal options object, and provides defaults.
|
||||
* @param {Object} opts - Options object
|
||||
*/
|
||||
function _applyRemainingDefaultOptions(opts) {
|
||||
opts.icon = Object.prototype.hasOwnProperty.call(opts, "icon") ? opts.icon : "\uE9CB"; // Accepts characters (and also URLs?), like '#', '¶', '❡', or '§'.
|
||||
opts.visible = Object.prototype.hasOwnProperty.call(opts, "visible") ? opts.visible : "hover"; // Also accepts 'always' & 'touch'
|
||||
opts.placement = Object.prototype.hasOwnProperty.call(opts, "placement") ? opts.placement : "right"; // Also accepts 'left'
|
||||
opts.ariaLabel = Object.prototype.hasOwnProperty.call(opts, "ariaLabel") ? opts.ariaLabel : "Anchor"; // Accepts any text.
|
||||
opts.class = Object.prototype.hasOwnProperty.call(opts, "class") ? opts.class : ""; // Accepts any class name.
|
||||
opts.base = Object.prototype.hasOwnProperty.call(opts, "base") ? opts.base : ""; // Accepts any base URI.
|
||||
opts.titleText = Object.prototype.hasOwnProperty.call(opts, "titleText") ? opts.titleText : ""; // Accepts any text.
|
||||
}
|
||||
|
||||
_applyRemainingDefaultOptions(this.options);
|
||||
|
||||
/**
|
||||
* Checks to see if this device supports touch. Uses criteria pulled from Modernizr:
|
||||
* https://github.com/Modernizr/Modernizr/blob/da22eb27631fc4957f67607fe6042e85c0a84656/feature-detects/touchevents.js#L40
|
||||
* @return {Boolean} - true if the current device supports touch.
|
||||
*/
|
||||
this.isTouchDevice = function () {
|
||||
// eslint-disable-next-line no-undef, compat/compat
|
||||
return Boolean("ontouchstart" in window || window.TouchEvent || window.DocumentTouch && document instanceof DocumentTouch);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add anchor links to page elements.
|
||||
* @param {String|Array|Nodelist} selector - A CSS selector for targeting the elements you wish to add anchor links
|
||||
* to. Also accepts an array or nodeList containing the relavant elements.
|
||||
* @return {this} - The AnchorJS object
|
||||
*/
|
||||
this.add = function (selector) {
|
||||
let elementID;
|
||||
let i;
|
||||
let anchor;
|
||||
let visibleOptionToUse;
|
||||
let hrefBase;
|
||||
const indexesToDrop = [];
|
||||
|
||||
// We reapply options here because somebody may have overwritten the default options object when setting options.
|
||||
// For example, this overwrites all options but visible:
|
||||
//
|
||||
// anchors.options = { visible: 'always'; }
|
||||
_applyRemainingDefaultOptions(this.options);
|
||||
|
||||
visibleOptionToUse = this.options.visible;
|
||||
if (visibleOptionToUse === "touch") {
|
||||
visibleOptionToUse = this.isTouchDevice() ? "always" : "hover";
|
||||
}
|
||||
|
||||
// Provide a sensible default selector, if none is given.
|
||||
if (!selector) {
|
||||
selector = "h2, h3, h4, h5, h6";
|
||||
}
|
||||
|
||||
const elements = _getElements(selector);
|
||||
|
||||
if (elements.length === 0) {
|
||||
return this;
|
||||
}
|
||||
|
||||
for (i = 0; i < elements.length; i++) {
|
||||
if (this.hasAnchorJSLink(elements[i])) {
|
||||
indexesToDrop.push(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (elements[i].hasAttribute("id")) {
|
||||
elementID = elements[i].getAttribute("id");
|
||||
} else {
|
||||
// Don't add anchor to elements without ID.
|
||||
indexesToDrop.push(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// The following code efficiently builds this DOM structure:
|
||||
// `<a class="anchorjs-link ${this.options.class}"
|
||||
// aria-label="${this.options.ariaLabel}"
|
||||
// title="${this.options.titleText}"
|
||||
// href="${this.options.base}#${elementID}">
|
||||
// ${this.options.icon}
|
||||
// </a>`
|
||||
anchor = document.createElement("a");
|
||||
anchor.className = "anchorjs-link " + this.options.class;
|
||||
anchor.setAttribute("aria-label", this.options.ariaLabel);
|
||||
anchor.innerText = this.options.icon;
|
||||
if (this.options.titleText) {
|
||||
anchor.title = this.options.titleText;
|
||||
}
|
||||
|
||||
// Adjust the href if there's a <base> tag. See https://github.com/bryanbraun/anchorjs/issues/98
|
||||
hrefBase = document.querySelector("base") ? window.location.pathname + window.location.search : "";
|
||||
hrefBase = this.options.base || hrefBase;
|
||||
anchor.href = hrefBase + "#" + elementID;
|
||||
|
||||
if (visibleOptionToUse === "always") {
|
||||
anchor.style.opacity = "1";
|
||||
}
|
||||
|
||||
if (this.options.placement === "left") {
|
||||
elements[i].insertBefore(anchor, elements[i].firstChild);
|
||||
} else { // if the option provided is `right` (or anything else).
|
||||
elements[i].appendChild(anchor);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < indexesToDrop.length; i++) {
|
||||
elements.splice(indexesToDrop[i] - i, 1);
|
||||
}
|
||||
|
||||
this.elements = this.elements.concat(elements);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all anchorjs-links from elements targeted by the selector.
|
||||
* @param {String|Array|Nodelist} selector - A CSS selector string targeting elements with anchor links,
|
||||
* OR a nodeList / array containing the DOM elements.
|
||||
* @return {this} - The AnchorJS object
|
||||
*/
|
||||
this.remove = function (selector) {
|
||||
let index;
|
||||
let domAnchor;
|
||||
const elements = _getElements(selector);
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
domAnchor = elements[i].querySelector(".anchorjs-link");
|
||||
if (domAnchor) {
|
||||
// Drop the element from our main list, if it's in there.
|
||||
index = this.elements.indexOf(elements[i]);
|
||||
if (index !== -1) {
|
||||
this.elements.splice(index, 1);
|
||||
}
|
||||
|
||||
// Remove the anchor from the DOM.
|
||||
elements[i].removeChild(domAnchor);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all anchorjs links. Mostly used for tests.
|
||||
*/
|
||||
this.removeAll = function () {
|
||||
this.remove(this.elements);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if this element already has an AnchorJS link on it.
|
||||
* Uses this technique: https://stackoverflow.com/a/5898748/1154642
|
||||
* @param {HTMLElement} el - a DOM node
|
||||
* @return {Boolean} true/false
|
||||
*/
|
||||
this.hasAnchorJSLink = function (el) {
|
||||
const hasLeftAnchor = el.firstChild && (" " + el.firstChild.className + " ").indexOf(" anchorjs-link ") > -1;
|
||||
const hasRightAnchor = el.lastChild && (" " + el.lastChild.className + " ").indexOf(" anchorjs-link ") > -1;
|
||||
|
||||
return hasLeftAnchor || hasRightAnchor || false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns a selector, nodeList, or array of elements into an array of elements (so we can use array methods).
|
||||
* It also throws errors on any other inputs. Used to handle inputs to .add and .remove.
|
||||
* @param {String|Array|Nodelist} input - A CSS selector string targeting elements with anchor links,
|
||||
* OR a nodeList / array containing the DOM elements.
|
||||
* @return {Array} - An array containing the elements we want.
|
||||
*/
|
||||
function _getElements(input) {
|
||||
let elements;
|
||||
if (typeof input === "string" || input instanceof String) {
|
||||
// See https://davidwalsh.name/nodelist-array for the technique transforming nodeList -> Array.
|
||||
elements = [].slice.call(document.querySelectorAll(input));
|
||||
// I checked the 'input instanceof NodeList' test in IE9 and modern browsers and it worked for me.
|
||||
} else if (Array.isArray(input) || input instanceof NodeList) {
|
||||
elements = [].slice.call(input);
|
||||
} else {
|
||||
throw new TypeError("The selector provided to AnchorJS was invalid.");
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
};
|
||||
|
||||
export default SimpleAnchor;
|
Loading…
x
Reference in New Issue
Block a user