1
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:
Jake Jarvis 2021-08-30 13:24:15 -04:00
commit 057a3f87b1
Signed by: jake
GPG Key ID: 2B0C9CF251E69A39
10 changed files with 4509 additions and 0 deletions

17
.eslintrc.json Normal file
View 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
View 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
View File

@ -0,0 +1,5 @@
.DS_Store
node_modules/
dist/
.npmrc
.vscode/

21
LICENSE Normal file
View 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
View File

@ -0,0 +1,53 @@
# 🔗 Simple Anchor [![npm](https://img.shields.io/npm/v/simple-anchor?logo=npm)](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
View 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
View 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
View 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
View 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;

4017
yarn.lock Normal file

File diff suppressed because it is too large Load Diff