@haprompt/link
Advanced tools
| /** | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
| 'use strict'; | ||
| var utils = require('@haprompt/utils'); | ||
| var haprompt = require('haprompt'); | ||
| /** @module @haprompt/link */ | ||
| const SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'sms:', 'tel:']); | ||
| /** @noInheritDoc */ | ||
| class LinkNode extends haprompt.ElementNode { | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| /** @internal */ | ||
| static getType() { | ||
| return 'link'; | ||
| } | ||
| static clone(node) { | ||
| return new LinkNode(node.__url, { | ||
| rel: node.__rel, | ||
| target: node.__target, | ||
| title: node.__title | ||
| }, node.__key); | ||
| } | ||
| constructor(url, attributes = {}, key) { | ||
| super(key); | ||
| const { | ||
| target = null, | ||
| rel = null, | ||
| title = null | ||
| } = attributes; | ||
| this.__url = url; | ||
| this.__target = target; | ||
| this.__rel = rel; | ||
| this.__title = title; | ||
| } | ||
| createDOM(config) { | ||
| const element = document.createElement('a'); | ||
| element.href = this.sanitizeUrl(this.__url); | ||
| if (this.__target !== null) { | ||
| element.target = this.__target; | ||
| } | ||
| if (this.__rel !== null) { | ||
| element.rel = this.__rel; | ||
| } | ||
| if (this.__title !== null) { | ||
| element.title = this.__title; | ||
| } | ||
| utils.addClassNamesToElement(element, config.theme.link); | ||
| return element; | ||
| } | ||
| updateDOM(prevNode, anchor, config) { | ||
| const url = this.__url; | ||
| const target = this.__target; | ||
| const rel = this.__rel; | ||
| const title = this.__title; | ||
| if (url !== prevNode.__url) { | ||
| anchor.href = url; | ||
| } | ||
| if (target !== prevNode.__target) { | ||
| if (target) { | ||
| anchor.target = target; | ||
| } else { | ||
| anchor.removeAttribute('target'); | ||
| } | ||
| } | ||
| if (rel !== prevNode.__rel) { | ||
| if (rel) { | ||
| anchor.rel = rel; | ||
| } else { | ||
| anchor.removeAttribute('rel'); | ||
| } | ||
| } | ||
| if (title !== prevNode.__title) { | ||
| if (title) { | ||
| anchor.title = title; | ||
| } else { | ||
| anchor.removeAttribute('title'); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| static importDOM() { | ||
| return { | ||
| a: node => ({ | ||
| conversion: convertAnchorElement, | ||
| priority: 1 | ||
| }) | ||
| }; | ||
| } | ||
| static importJSON(serializedNode) { | ||
| const node = $createLinkNode(serializedNode.url, { | ||
| rel: serializedNode.rel, | ||
| target: serializedNode.target, | ||
| title: serializedNode.title | ||
| }); | ||
| node.setFormat(serializedNode.format); | ||
| node.setIndent(serializedNode.indent); | ||
| node.setDirection(serializedNode.direction); | ||
| return node; | ||
| } | ||
| sanitizeUrl(url) { | ||
| try { | ||
| const parsedUrl = new URL(url); // eslint-disable-next-line no-script-url | ||
| if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) { | ||
| return 'about:blank'; | ||
| } | ||
| } catch { | ||
| return url; | ||
| } | ||
| return url; | ||
| } | ||
| exportJSON() { | ||
| return { ...super.exportJSON(), | ||
| rel: this.getRel(), | ||
| target: this.getTarget(), | ||
| title: this.getTitle(), | ||
| type: 'link', | ||
| url: this.getURL(), | ||
| version: 1 | ||
| }; | ||
| } | ||
| getURL() { | ||
| return this.getLatest().__url; | ||
| } | ||
| setURL(url) { | ||
| const writable = this.getWritable(); | ||
| writable.__url = url; | ||
| } | ||
| getTarget() { | ||
| return this.getLatest().__target; | ||
| } | ||
| setTarget(target) { | ||
| const writable = this.getWritable(); | ||
| writable.__target = target; | ||
| } | ||
| getRel() { | ||
| return this.getLatest().__rel; | ||
| } | ||
| setRel(rel) { | ||
| const writable = this.getWritable(); | ||
| writable.__rel = rel; | ||
| } | ||
| getTitle() { | ||
| return this.getLatest().__title; | ||
| } | ||
| setTitle(title) { | ||
| const writable = this.getWritable(); | ||
| writable.__title = title; | ||
| } | ||
| insertNewAfter(selection, restoreSelection = true) { | ||
| const element = this.getParentOrThrow().insertNewAfter(selection, restoreSelection); | ||
| if (haprompt.$isElementNode(element)) { | ||
| const linkNode = $createLinkNode(this.__url, { | ||
| rel: this.__rel, | ||
| target: this.__target, | ||
| title: this.__title | ||
| }); | ||
| element.append(linkNode); | ||
| return linkNode; | ||
| } | ||
| return null; | ||
| } | ||
| canInsertTextBefore() { | ||
| return false; | ||
| } | ||
| canInsertTextAfter() { | ||
| return false; | ||
| } | ||
| canBeEmpty() { | ||
| return false; | ||
| } | ||
| isInline() { | ||
| return true; | ||
| } | ||
| extractWithChild(child, selection, destination) { | ||
| if (!haprompt.$isRangeSelection(selection)) { | ||
| return false; | ||
| } | ||
| const anchorNode = selection.anchor.getNode(); | ||
| const focusNode = selection.focus.getNode(); | ||
| return this.isParentOf(anchorNode) && this.isParentOf(focusNode) && selection.getTextContent().length > 0; | ||
| } | ||
| } | ||
| function convertAnchorElement(domNode) { | ||
| let node = null; | ||
| if (utils.isHTMLAnchorElement(domNode)) { | ||
| const content = domNode.textContent; | ||
| if (content !== null && content !== '') { | ||
| node = $createLinkNode(domNode.getAttribute('href') || '', { | ||
| rel: domNode.getAttribute('rel'), | ||
| target: domNode.getAttribute('target'), | ||
| title: domNode.getAttribute('title') | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| node | ||
| }; | ||
| } | ||
| /** | ||
| * Takes a URL and creates a LinkNode. | ||
| * @param url - The URL the LinkNode should direct to. | ||
| * @param attributes - Optional HTML a tag attributes { target, rel, title } | ||
| * @returns The LinkNode. | ||
| */ | ||
| function $createLinkNode(url, attributes) { | ||
| return haprompt.$applyNodeReplacement(new LinkNode(url, attributes)); | ||
| } | ||
| /** | ||
| * Determines if node is a LinkNode. | ||
| * @param node - The node to be checked. | ||
| * @returns true if node is a LinkNode, false otherwise. | ||
| */ | ||
| function $isLinkNode(node) { | ||
| return node instanceof LinkNode; | ||
| } | ||
| // Custom node type to override `canInsertTextAfter` that will | ||
| // allow typing within the link | ||
| class AutoLinkNode extends LinkNode { | ||
| static getType() { | ||
| return 'autolink'; | ||
| } | ||
| static clone(node) { | ||
| return new AutoLinkNode(node.__url, { | ||
| rel: node.__rel, | ||
| target: node.__target, | ||
| title: node.__title | ||
| }, node.__key); | ||
| } | ||
| static importJSON(serializedNode) { | ||
| const node = $createAutoLinkNode(serializedNode.url, { | ||
| rel: serializedNode.rel, | ||
| target: serializedNode.target, | ||
| title: serializedNode.title | ||
| }); | ||
| node.setFormat(serializedNode.format); | ||
| node.setIndent(serializedNode.indent); | ||
| node.setDirection(serializedNode.direction); | ||
| return node; | ||
| } | ||
| static importDOM() { | ||
| // TODO: Should link node should handle the import over autolink? | ||
| return null; | ||
| } | ||
| exportJSON() { | ||
| return { ...super.exportJSON(), | ||
| type: 'autolink', | ||
| version: 1 | ||
| }; | ||
| } | ||
| insertNewAfter(selection, restoreSelection = true) { | ||
| const element = this.getParentOrThrow().insertNewAfter(selection, restoreSelection); | ||
| if (haprompt.$isElementNode(element)) { | ||
| const linkNode = $createAutoLinkNode(this.__url, { | ||
| rel: this._rel, | ||
| target: this.__target, | ||
| title: this.__title | ||
| }); | ||
| element.append(linkNode); | ||
| return linkNode; | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Takes a URL and creates an AutoLinkNode. AutoLinkNodes are generally automatically generated | ||
| * during typing, which is especially useful when a button to generate a LinkNode is not practical. | ||
| * @param url - The URL the LinkNode should direct to. | ||
| * @param attributes - Optional HTML a tag attributes. { target, rel, title } | ||
| * @returns The LinkNode. | ||
| */ | ||
| function $createAutoLinkNode(url, attributes) { | ||
| return haprompt.$applyNodeReplacement(new AutoLinkNode(url, attributes)); | ||
| } | ||
| /** | ||
| * Determines if node is an AutoLinkNode. | ||
| * @param node - The node to be checked. | ||
| * @returns true if node is an AutoLinkNode, false otherwise. | ||
| */ | ||
| function $isAutoLinkNode(node) { | ||
| return node instanceof AutoLinkNode; | ||
| } | ||
| const TOGGLE_LINK_COMMAND = haprompt.createCommand('TOGGLE_LINK_COMMAND'); | ||
| /** | ||
| * Generates or updates a LinkNode. It can also delete a LinkNode if the URL is null, | ||
| * but saves any children and brings them up to the parent node. | ||
| * @param url - The URL the link directs to. | ||
| * @param attributes - Optional HTML a tag attributes. { target, rel, title } | ||
| */ | ||
| function toggleLink(url, attributes = {}) { | ||
| const { | ||
| target, | ||
| title | ||
| } = attributes; | ||
| const rel = attributes.rel === undefined ? 'noreferrer' : attributes.rel; | ||
| const selection = haprompt.$getSelection(); | ||
| if (!haprompt.$isRangeSelection(selection)) { | ||
| return; | ||
| } | ||
| const nodes = selection.extract(); | ||
| if (url === null) { | ||
| // Remove LinkNodes | ||
| nodes.forEach(node => { | ||
| const parent = node.getParent(); | ||
| if ($isLinkNode(parent)) { | ||
| const children = parent.getChildren(); | ||
| for (let i = 0; i < children.length; i++) { | ||
| parent.insertBefore(children[i]); | ||
| } | ||
| parent.remove(); | ||
| } | ||
| }); | ||
| } else { | ||
| // Add or merge LinkNodes | ||
| if (nodes.length === 1) { | ||
| const firstNode = nodes[0]; // if the first node is a LinkNode or if its | ||
| // parent is a LinkNode, we update the URL, target and rel. | ||
| const linkNode = $isLinkNode(firstNode) ? firstNode : $getLinkAncestor(firstNode); | ||
| if (linkNode !== null) { | ||
| linkNode.setURL(url); | ||
| if (target !== undefined) { | ||
| linkNode.setTarget(target); | ||
| } | ||
| if (rel !== null) { | ||
| linkNode.setRel(rel); | ||
| } | ||
| if (title !== undefined) { | ||
| linkNode.setTitle(title); | ||
| } | ||
| return; | ||
| } | ||
| } | ||
| let prevParent = null; | ||
| let linkNode = null; | ||
| nodes.forEach(node => { | ||
| const parent = node.getParent(); | ||
| if (parent === linkNode || parent === null || haprompt.$isElementNode(node) && !node.isInline()) { | ||
| return; | ||
| } | ||
| if ($isLinkNode(parent)) { | ||
| linkNode = parent; | ||
| parent.setURL(url); | ||
| if (target !== undefined) { | ||
| parent.setTarget(target); | ||
| } | ||
| if (rel !== null) { | ||
| linkNode.setRel(rel); | ||
| } | ||
| if (title !== undefined) { | ||
| linkNode.setTitle(title); | ||
| } | ||
| return; | ||
| } | ||
| if (!parent.is(prevParent)) { | ||
| prevParent = parent; | ||
| linkNode = $createLinkNode(url, { | ||
| rel, | ||
| target | ||
| }); | ||
| if ($isLinkNode(parent)) { | ||
| if (node.getPreviousSibling() === null) { | ||
| parent.insertBefore(linkNode); | ||
| } else { | ||
| parent.insertAfter(linkNode); | ||
| } | ||
| } else { | ||
| node.insertBefore(linkNode); | ||
| } | ||
| } | ||
| if ($isLinkNode(node)) { | ||
| if (node.is(linkNode)) { | ||
| return; | ||
| } | ||
| if (linkNode !== null) { | ||
| const children = node.getChildren(); | ||
| for (let i = 0; i < children.length; i++) { | ||
| linkNode.append(children[i]); | ||
| } | ||
| } | ||
| node.remove(); | ||
| return; | ||
| } | ||
| if (linkNode !== null) { | ||
| linkNode.append(node); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| function $getLinkAncestor(node) { | ||
| return $getAncestor(node, $isLinkNode); | ||
| } | ||
| function $getAncestor(node, predicate) { | ||
| let parent = node; | ||
| while (parent !== null && (parent = parent.getParent()) !== null && !predicate(parent)); | ||
| return parent; | ||
| } | ||
| exports.$createAutoLinkNode = $createAutoLinkNode; | ||
| exports.$createLinkNode = $createLinkNode; | ||
| exports.$isAutoLinkNode = $isAutoLinkNode; | ||
| exports.$isLinkNode = $isLinkNode; | ||
| exports.AutoLinkNode = AutoLinkNode; | ||
| exports.LinkNode = LinkNode; | ||
| exports.TOGGLE_LINK_COMMAND = TOGGLE_LINK_COMMAND; | ||
| exports.toggleLink = toggleLink; |
Sorry, the diff of this file is not supported yet
| /** | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
| 'use strict';var k=require("@haprompt/utils"),n=require("haprompt");let p=new Set(["http:","https:","mailto:","sms:","tel:"]); | ||
| class q extends n.ElementNode{static getType(){return"link"}static clone(a){return new q(a.__url,{rel:a.__rel,target:a.__target,title:a.__title},a.__key)}constructor(a,b={},d){super(d);let {target:l=null,rel:h=null,title:e=null}=b;this.__url=a;this.__target=l;this.__rel=h;this.__title=e}createDOM(a){let b=document.createElement("a");b.href=this.sanitizeUrl(this.__url);null!==this.__target&&(b.target=this.__target);null!==this.__rel&&(b.rel=this.__rel);null!==this.__title&&(b.title=this.__title);k.addClassNamesToElement(b, | ||
| a.theme.link);return b}updateDOM(a,b){let d=this.__url,l=this.__target,h=this.__rel,e=this.__title;d!==a.__url&&(b.href=d);l!==a.__target&&(l?b.target=l:b.removeAttribute("target"));h!==a.__rel&&(h?b.rel=h:b.removeAttribute("rel"));e!==a.__title&&(e?b.title=e:b.removeAttribute("title"));return!1}static importDOM(){return{a:()=>({conversion:r,priority:1})}}static importJSON(a){let b=t(a.url,{rel:a.rel,target:a.target,title:a.title});b.setFormat(a.format);b.setIndent(a.indent);b.setDirection(a.direction); | ||
| return b}sanitizeUrl(a){try{let b=new URL(a);if(!p.has(b.protocol))return"about:blank"}catch{}return a}exportJSON(){return{...super.exportJSON(),rel:this.getRel(),target:this.getTarget(),title:this.getTitle(),type:"link",url:this.getURL(),version:1}}getURL(){return this.getLatest().__url}setURL(a){this.getWritable().__url=a}getTarget(){return this.getLatest().__target}setTarget(a){this.getWritable().__target=a}getRel(){return this.getLatest().__rel}setRel(a){this.getWritable().__rel=a}getTitle(){return this.getLatest().__title}setTitle(a){this.getWritable().__title= | ||
| a}insertNewAfter(a,b=!0){a=this.getParentOrThrow().insertNewAfter(a,b);return n.$isElementNode(a)?(b=t(this.__url,{rel:this.__rel,target:this.__target,title:this.__title}),a.append(b),b):null}canInsertTextBefore(){return!1}canInsertTextAfter(){return!1}canBeEmpty(){return!1}isInline(){return!0}extractWithChild(a,b){if(!n.$isRangeSelection(b))return!1;a=b.anchor.getNode();let d=b.focus.getNode();return this.isParentOf(a)&&this.isParentOf(d)&&0<b.getTextContent().length}} | ||
| function r(a){let b=null;if(k.isHTMLAnchorElement(a)){let d=a.textContent;null!==d&&""!==d&&(b=t(a.getAttribute("href")||"",{rel:a.getAttribute("rel"),target:a.getAttribute("target"),title:a.getAttribute("title")}))}return{node:b}}function t(a,b){return n.$applyNodeReplacement(new q(a,b))}function v(a){return a instanceof q} | ||
| class w extends q{static getType(){return"autolink"}static clone(a){return new w(a.__url,{rel:a.__rel,target:a.__target,title:a.__title},a.__key)}static importJSON(a){let b=x(a.url,{rel:a.rel,target:a.target,title:a.title});b.setFormat(a.format);b.setIndent(a.indent);b.setDirection(a.direction);return b}static importDOM(){return null}exportJSON(){return{...super.exportJSON(),type:"autolink",version:1}}insertNewAfter(a,b=!0){a=this.getParentOrThrow().insertNewAfter(a,b);return n.$isElementNode(a)? | ||
| (b=x(this.__url,{rel:this._rel,target:this.__target,title:this.__title}),a.append(b),b):null}}function x(a,b){return n.$applyNodeReplacement(new w(a,b))}let y=n.createCommand("TOGGLE_LINK_COMMAND");function z(a,b){for(;null!==a&&null!==(a=a.getParent())&&!b(a););return a}exports.$createAutoLinkNode=x;exports.$createLinkNode=t;exports.$isAutoLinkNode=function(a){return a instanceof w};exports.$isLinkNode=v;exports.AutoLinkNode=w;exports.LinkNode=q;exports.TOGGLE_LINK_COMMAND=y; | ||
| exports.toggleLink=function(a,b={}){let {target:d,title:l}=b,h=void 0===b.rel?"noreferrer":b.rel;b=n.$getSelection();if(n.$isRangeSelection(b))if(b=b.extract(),null===a)b.forEach(m=>{m=m.getParent();if(v(m)){let c=m.getChildren();for(let f=0;f<c.length;f++)m.insertBefore(c[f]);m.remove()}});else{if(1===b.length){var e=b[0];e=v(e)?e:z(e,v);if(null!==e){e.setURL(a);void 0!==d&&e.setTarget(d);null!==h&&e.setRel(h);void 0!==l&&e.setTitle(l);return}}let m=null,c=null;b.forEach(f=>{var g=f.getParent(); | ||
| if(g!==c&&null!==g&&(!n.$isElementNode(f)||f.isInline()))if(v(g))c=g,g.setURL(a),void 0!==d&&g.setTarget(d),null!==h&&c.setRel(h),void 0!==l&&c.setTitle(l);else if(g.is(m)||(m=g,c=t(a,{rel:h,target:d}),v(g)?null===f.getPreviousSibling()?g.insertBefore(c):g.insertAfter(c):f.insertBefore(c)),v(f)){if(!f.is(c)){if(null!==c){g=f.getChildren();for(let u=0;u<g.length;u++)c.append(g[u])}f.remove()}}else null!==c&&c.append(f)})}} |
+98
| /** @module @haprompt/link */ | ||
| /** | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
| import type { DOMConversionMap, EditorConfig, GridSelection, HapromptCommand, HapromptNode, NodeKey, NodeSelection, RangeSelection, SerializedElementNode } from 'haprompt'; | ||
| import { ElementNode, Spread } from 'haprompt'; | ||
| export type LinkAttributes = { | ||
| rel?: null | string; | ||
| target?: null | string; | ||
| title?: null | string; | ||
| }; | ||
| export type SerializedLinkNode = Spread<{ | ||
| url: string; | ||
| }, Spread<LinkAttributes, SerializedElementNode>>; | ||
| /** @noInheritDoc */ | ||
| export declare class LinkNode extends ElementNode { | ||
| /** @internal */ | ||
| __url: string; | ||
| /** @internal */ | ||
| __target: null | string; | ||
| /** @internal */ | ||
| __rel: null | string; | ||
| /** @internal */ | ||
| __title: null | string; | ||
| static getType(): string; | ||
| static clone(node: LinkNode): LinkNode; | ||
| constructor(url: string, attributes?: LinkAttributes, key?: NodeKey); | ||
| createDOM(config: EditorConfig): HTMLAnchorElement; | ||
| updateDOM(prevNode: LinkNode, anchor: HTMLAnchorElement, config: EditorConfig): boolean; | ||
| static importDOM(): DOMConversionMap | null; | ||
| static importJSON(serializedNode: SerializedLinkNode | SerializedAutoLinkNode): LinkNode; | ||
| sanitizeUrl(url: string): string; | ||
| exportJSON(): SerializedLinkNode | SerializedAutoLinkNode; | ||
| getURL(): string; | ||
| setURL(url: string): void; | ||
| getTarget(): null | string; | ||
| setTarget(target: null | string): void; | ||
| getRel(): null | string; | ||
| setRel(rel: null | string): void; | ||
| getTitle(): null | string; | ||
| setTitle(title: null | string): void; | ||
| insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): null | ElementNode; | ||
| canInsertTextBefore(): false; | ||
| canInsertTextAfter(): false; | ||
| canBeEmpty(): false; | ||
| isInline(): true; | ||
| extractWithChild(child: HapromptNode, selection: RangeSelection | NodeSelection | GridSelection, destination: 'clone' | 'html'): boolean; | ||
| } | ||
| /** | ||
| * Takes a URL and creates a LinkNode. | ||
| * @param url - The URL the LinkNode should direct to. | ||
| * @param attributes - Optional HTML a tag attributes { target, rel, title } | ||
| * @returns The LinkNode. | ||
| */ | ||
| export declare function $createLinkNode(url: string, attributes?: LinkAttributes): LinkNode; | ||
| /** | ||
| * Determines if node is a LinkNode. | ||
| * @param node - The node to be checked. | ||
| * @returns true if node is a LinkNode, false otherwise. | ||
| */ | ||
| export declare function $isLinkNode(node: HapromptNode | null | undefined): node is LinkNode; | ||
| export type SerializedAutoLinkNode = SerializedLinkNode; | ||
| export declare class AutoLinkNode extends LinkNode { | ||
| static getType(): string; | ||
| static clone(node: AutoLinkNode): AutoLinkNode; | ||
| static importJSON(serializedNode: SerializedAutoLinkNode): AutoLinkNode; | ||
| static importDOM(): null; | ||
| exportJSON(): SerializedAutoLinkNode; | ||
| insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): null | ElementNode; | ||
| } | ||
| /** | ||
| * Takes a URL and creates an AutoLinkNode. AutoLinkNodes are generally automatically generated | ||
| * during typing, which is especially useful when a button to generate a LinkNode is not practical. | ||
| * @param url - The URL the LinkNode should direct to. | ||
| * @param attributes - Optional HTML a tag attributes. { target, rel, title } | ||
| * @returns The LinkNode. | ||
| */ | ||
| export declare function $createAutoLinkNode(url: string, attributes?: LinkAttributes): AutoLinkNode; | ||
| /** | ||
| * Determines if node is an AutoLinkNode. | ||
| * @param node - The node to be checked. | ||
| * @returns true if node is an AutoLinkNode, false otherwise. | ||
| */ | ||
| export declare function $isAutoLinkNode(node: HapromptNode | null | undefined): node is AutoLinkNode; | ||
| export declare const TOGGLE_LINK_COMMAND: HapromptCommand<string | ({ | ||
| url: string; | ||
| } & LinkAttributes) | null>; | ||
| /** | ||
| * Generates or updates a LinkNode. It can also delete a LinkNode if the URL is null, | ||
| * but saves any children and brings them up to the parent node. | ||
| * @param url - The URL the link directs to. | ||
| * @param attributes - Optional HTML a tag attributes. { target, rel, title } | ||
| */ | ||
| export declare function toggleLink(url: null | string, attributes?: LinkAttributes): void; |
+21
| MIT License | ||
| Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| 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. |
+3
-5
@@ -6,7 +6,5 @@ /** | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
| 'use strict'; | ||
| module.exports = require('./dist/HapromptLink.js'); | ||
| 'use strict' | ||
| const HapromptLink = process.env.NODE_ENV === 'development' ? require('./HapromptLink.dev.js') : require('./HapromptLink.prod.js') | ||
| module.exports = HapromptLink; |
+3
-3
@@ -11,9 +11,9 @@ { | ||
| "license": "MIT", | ||
| "version": "0.12.0", | ||
| "version": "0.12.6", | ||
| "main": "HapromptLink.js", | ||
| "peerDependencies": { | ||
| "haprompt": "0.12.0" | ||
| "haprompt": "0.12.6" | ||
| }, | ||
| "dependencies": { | ||
| "@haprompt/utils": "0.12.0" | ||
| "@haprompt/utils": "0.12.6" | ||
| }, | ||
@@ -20,0 +20,0 @@ "repository": { |
Sorry, the diff of this file is not supported yet
| /** | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
| import {$createLinkNode, $isLinkNode, LinkNode} from '@haprompt/link'; | ||
| import {initializeUnitTest} from 'haprompt/src/__tests__/utils'; | ||
| const editorConfig = Object.freeze({ | ||
| namespace: '', | ||
| theme: { | ||
| link: 'my-link-class', | ||
| text: { | ||
| bold: 'my-bold-class', | ||
| code: 'my-code-class', | ||
| hashtag: 'my-hashtag-class', | ||
| italic: 'my-italic-class', | ||
| strikethrough: 'my-strikethrough-class', | ||
| underline: 'my-underline-class', | ||
| underlineStrikethrough: 'my-underline-strikethrough-class', | ||
| }, | ||
| }, | ||
| }); | ||
| describe('HapromptLinkNode tests', () => { | ||
| initializeUnitTest((testEnv) => { | ||
| test('LinkNode.constructor', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('/'); | ||
| expect(linkNode.__type).toBe('link'); | ||
| expect(linkNode.__url).toBe('/'); | ||
| }); | ||
| expect(() => new LinkNode('')).toThrow(); | ||
| }); | ||
| test('LineBreakNode.clone()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('/'); | ||
| const linkNodeClone = LinkNode.clone(linkNode); | ||
| expect(linkNodeClone).not.toBe(linkNode); | ||
| expect(linkNodeClone).toStrictEqual(linkNode); | ||
| }); | ||
| }); | ||
| test('LinkNode.getURL()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo'); | ||
| expect(linkNode.getURL()).toBe('https://example.com/foo'); | ||
| }); | ||
| }); | ||
| test('LinkNode.setURL()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo'); | ||
| expect(linkNode.getURL()).toBe('https://example.com/foo'); | ||
| linkNode.setURL('https://example.com/bar'); | ||
| expect(linkNode.getURL()).toBe('https://example.com/bar'); | ||
| }); | ||
| }); | ||
| test('LinkNode.getTarget()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| target: '_blank', | ||
| }); | ||
| expect(linkNode.getTarget()).toBe('_blank'); | ||
| }); | ||
| }); | ||
| test('LinkNode.setTarget()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| target: '_blank', | ||
| }); | ||
| expect(linkNode.getTarget()).toBe('_blank'); | ||
| linkNode.setTarget('_self'); | ||
| expect(linkNode.getTarget()).toBe('_self'); | ||
| }); | ||
| }); | ||
| test('LinkNode.getRel()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| rel: 'noopener noreferrer', | ||
| target: '_blank', | ||
| }); | ||
| expect(linkNode.getRel()).toBe('noopener noreferrer'); | ||
| }); | ||
| }); | ||
| test('LinkNode.setRel()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| rel: 'noopener', | ||
| target: '_blank', | ||
| }); | ||
| expect(linkNode.getRel()).toBe('noopener'); | ||
| linkNode.setRel('noopener noreferrer'); | ||
| expect(linkNode.getRel()).toBe('noopener noreferrer'); | ||
| }); | ||
| }); | ||
| test('LinkNode.getTitle()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| title: 'Hello world', | ||
| }); | ||
| expect(linkNode.getTitle()).toBe('Hello world'); | ||
| }); | ||
| }); | ||
| test('LinkNode.setTitle()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| title: 'Hello world', | ||
| }); | ||
| expect(linkNode.getTitle()).toBe('Hello world'); | ||
| linkNode.setTitle('World hello'); | ||
| expect(linkNode.getTitle()).toBe('World hello'); | ||
| }); | ||
| }); | ||
| test('LinkNode.createDOM()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo'); | ||
| expect(linkNode.createDOM(editorConfig).outerHTML).toBe( | ||
| '<a href="https://example.com/foo" class="my-link-class"></a>', | ||
| ); | ||
| expect( | ||
| linkNode.createDOM({ | ||
| namespace: '', | ||
| theme: {}, | ||
| }).outerHTML, | ||
| ).toBe('<a href="https://example.com/foo"></a>'); | ||
| }); | ||
| }); | ||
| test('LinkNode.createDOM() with target, rel and title', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| rel: 'noopener noreferrer', | ||
| target: '_blank', | ||
| title: 'Hello world', | ||
| }); | ||
| expect(linkNode.createDOM(editorConfig).outerHTML).toBe( | ||
| '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-link-class"></a>', | ||
| ); | ||
| expect( | ||
| linkNode.createDOM({ | ||
| namespace: '', | ||
| theme: {}, | ||
| }).outerHTML, | ||
| ).toBe( | ||
| '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world"></a>', | ||
| ); | ||
| }); | ||
| }); | ||
| test('LinkNode.createDOM() sanitizes javascript: URLs', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| // eslint-disable-next-line no-script-url | ||
| const linkNode = new LinkNode('javascript:alert(0)'); | ||
| expect(linkNode.createDOM(editorConfig).outerHTML).toBe( | ||
| '<a href="about:blank" class="my-link-class"></a>', | ||
| ); | ||
| }); | ||
| }); | ||
| test('LinkNode.updateDOM()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo'); | ||
| const domElement = linkNode.createDOM(editorConfig); | ||
| expect(linkNode.createDOM(editorConfig).outerHTML).toBe( | ||
| '<a href="https://example.com/foo" class="my-link-class"></a>', | ||
| ); | ||
| const newLinkNode = new LinkNode('https://example.com/bar'); | ||
| const result = newLinkNode.updateDOM( | ||
| linkNode, | ||
| domElement, | ||
| editorConfig, | ||
| ); | ||
| expect(result).toBe(false); | ||
| expect(domElement.outerHTML).toBe( | ||
| '<a href="https://example.com/bar" class="my-link-class"></a>', | ||
| ); | ||
| }); | ||
| }); | ||
| test('LinkNode.updateDOM() with target, rel and title', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| rel: 'noopener noreferrer', | ||
| target: '_blank', | ||
| title: 'Hello world', | ||
| }); | ||
| const domElement = linkNode.createDOM(editorConfig); | ||
| expect(linkNode.createDOM(editorConfig).outerHTML).toBe( | ||
| '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-link-class"></a>', | ||
| ); | ||
| const newLinkNode = new LinkNode('https://example.com/bar', { | ||
| rel: 'noopener', | ||
| target: '_self', | ||
| title: 'World hello', | ||
| }); | ||
| const result = newLinkNode.updateDOM( | ||
| linkNode, | ||
| domElement, | ||
| editorConfig, | ||
| ); | ||
| expect(result).toBe(false); | ||
| expect(domElement.outerHTML).toBe( | ||
| '<a href="https://example.com/bar" target="_self" rel="noopener" title="World hello" class="my-link-class"></a>', | ||
| ); | ||
| }); | ||
| }); | ||
| test('LinkNode.updateDOM() with undefined target, undefined rel and undefined title', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| rel: 'noopener noreferrer', | ||
| target: '_blank', | ||
| title: 'Hello world', | ||
| }); | ||
| const domElement = linkNode.createDOM(editorConfig); | ||
| expect(linkNode.createDOM(editorConfig).outerHTML).toBe( | ||
| '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-link-class"></a>', | ||
| ); | ||
| const newLinkNode = new LinkNode('https://example.com/bar'); | ||
| const result = newLinkNode.updateDOM( | ||
| linkNode, | ||
| domElement, | ||
| editorConfig, | ||
| ); | ||
| expect(result).toBe(false); | ||
| expect(domElement.outerHTML).toBe( | ||
| '<a href="https://example.com/bar" class="my-link-class"></a>', | ||
| ); | ||
| }); | ||
| }); | ||
| test('LinkNode.canInsertTextBefore()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo'); | ||
| expect(linkNode.canInsertTextBefore()).toBe(false); | ||
| }); | ||
| }); | ||
| test('LinkNode.canInsertTextAfter()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo'); | ||
| expect(linkNode.canInsertTextAfter()).toBe(false); | ||
| }); | ||
| }); | ||
| test('$createLinkNode()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo'); | ||
| const createdLinkNode = $createLinkNode('https://example.com/foo'); | ||
| expect(linkNode.__type).toEqual(createdLinkNode.__type); | ||
| expect(linkNode.__parent).toEqual(createdLinkNode.__parent); | ||
| expect(linkNode.__url).toEqual(createdLinkNode.__url); | ||
| expect(linkNode.__key).not.toEqual(createdLinkNode.__key); | ||
| }); | ||
| }); | ||
| test('$createLinkNode() with target, rel and title', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode('https://example.com/foo', { | ||
| rel: 'noopener noreferrer', | ||
| target: '_blank', | ||
| title: 'Hello world', | ||
| }); | ||
| const createdLinkNode = $createLinkNode('https://example.com/foo', { | ||
| rel: 'noopener noreferrer', | ||
| target: '_blank', | ||
| title: 'Hello world', | ||
| }); | ||
| expect(linkNode.__type).toEqual(createdLinkNode.__type); | ||
| expect(linkNode.__parent).toEqual(createdLinkNode.__parent); | ||
| expect(linkNode.__url).toEqual(createdLinkNode.__url); | ||
| expect(linkNode.__target).toEqual(createdLinkNode.__target); | ||
| expect(linkNode.__rel).toEqual(createdLinkNode.__rel); | ||
| expect(linkNode.__title).toEqual(createdLinkNode.__title); | ||
| expect(linkNode.__key).not.toEqual(createdLinkNode.__key); | ||
| }); | ||
| }); | ||
| test('$isLinkNode()', async () => { | ||
| const {editor} = testEnv; | ||
| await editor.update(() => { | ||
| const linkNode = new LinkNode(''); | ||
| expect($isLinkNode(linkNode)).toBe(true); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
-553
| /** @module @haprompt/link */ | ||
| /** | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
| import type { | ||
| DOMConversionMap, | ||
| DOMConversionOutput, | ||
| EditorConfig, | ||
| GridSelection, | ||
| HapromptCommand, | ||
| HapromptNode, | ||
| NodeKey, | ||
| NodeSelection, | ||
| RangeSelection, | ||
| SerializedElementNode, | ||
| } from 'haprompt'; | ||
| import {addClassNamesToElement, isHTMLAnchorElement} from '@haprompt/utils'; | ||
| import { | ||
| $applyNodeReplacement, | ||
| $getSelection, | ||
| $isElementNode, | ||
| $isRangeSelection, | ||
| createCommand, | ||
| ElementNode, | ||
| Spread, | ||
| } from 'haprompt'; | ||
| export type LinkAttributes = { | ||
| rel?: null | string; | ||
| target?: null | string; | ||
| title?: null | string; | ||
| }; | ||
| export type SerializedLinkNode = Spread< | ||
| { | ||
| url: string; | ||
| }, | ||
| Spread<LinkAttributes, SerializedElementNode> | ||
| >; | ||
| const SUPPORTED_URL_PROTOCOLS = new Set([ | ||
| 'http:', | ||
| 'https:', | ||
| 'mailto:', | ||
| 'sms:', | ||
| 'tel:', | ||
| ]); | ||
| /** @noInheritDoc */ | ||
| export class LinkNode extends ElementNode { | ||
| /** @internal */ | ||
| __url: string; | ||
| /** @internal */ | ||
| __target: null | string; | ||
| /** @internal */ | ||
| __rel: null | string; | ||
| /** @internal */ | ||
| __title: null | string; | ||
| static getType(): string { | ||
| return 'link'; | ||
| } | ||
| static clone(node: LinkNode): LinkNode { | ||
| return new LinkNode( | ||
| node.__url, | ||
| {rel: node.__rel, target: node.__target, title: node.__title}, | ||
| node.__key, | ||
| ); | ||
| } | ||
| constructor(url: string, attributes: LinkAttributes = {}, key?: NodeKey) { | ||
| super(key); | ||
| const {target = null, rel = null, title = null} = attributes; | ||
| this.__url = url; | ||
| this.__target = target; | ||
| this.__rel = rel; | ||
| this.__title = title; | ||
| } | ||
| createDOM(config: EditorConfig): HTMLAnchorElement { | ||
| const element = document.createElement('a'); | ||
| element.href = this.sanitizeUrl(this.__url); | ||
| if (this.__target !== null) { | ||
| element.target = this.__target; | ||
| } | ||
| if (this.__rel !== null) { | ||
| element.rel = this.__rel; | ||
| } | ||
| if (this.__title !== null) { | ||
| element.title = this.__title; | ||
| } | ||
| addClassNamesToElement(element, config.theme.link); | ||
| return element; | ||
| } | ||
| updateDOM( | ||
| prevNode: LinkNode, | ||
| anchor: HTMLAnchorElement, | ||
| config: EditorConfig, | ||
| ): boolean { | ||
| const url = this.__url; | ||
| const target = this.__target; | ||
| const rel = this.__rel; | ||
| const title = this.__title; | ||
| if (url !== prevNode.__url) { | ||
| anchor.href = url; | ||
| } | ||
| if (target !== prevNode.__target) { | ||
| if (target) { | ||
| anchor.target = target; | ||
| } else { | ||
| anchor.removeAttribute('target'); | ||
| } | ||
| } | ||
| if (rel !== prevNode.__rel) { | ||
| if (rel) { | ||
| anchor.rel = rel; | ||
| } else { | ||
| anchor.removeAttribute('rel'); | ||
| } | ||
| } | ||
| if (title !== prevNode.__title) { | ||
| if (title) { | ||
| anchor.title = title; | ||
| } else { | ||
| anchor.removeAttribute('title'); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| static importDOM(): DOMConversionMap | null { | ||
| return { | ||
| a: (node: Node) => ({ | ||
| conversion: convertAnchorElement, | ||
| priority: 1, | ||
| }), | ||
| }; | ||
| } | ||
| static importJSON( | ||
| serializedNode: SerializedLinkNode | SerializedAutoLinkNode, | ||
| ): LinkNode { | ||
| const node = $createLinkNode(serializedNode.url, { | ||
| rel: serializedNode.rel, | ||
| target: serializedNode.target, | ||
| title: serializedNode.title, | ||
| }); | ||
| node.setFormat(serializedNode.format); | ||
| node.setIndent(serializedNode.indent); | ||
| node.setDirection(serializedNode.direction); | ||
| return node; | ||
| } | ||
| sanitizeUrl(url: string): string { | ||
| try { | ||
| const parsedUrl = new URL(url); | ||
| // eslint-disable-next-line no-script-url | ||
| if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) { | ||
| return 'about:blank'; | ||
| } | ||
| } catch { | ||
| return url; | ||
| } | ||
| return url; | ||
| } | ||
| exportJSON(): SerializedLinkNode | SerializedAutoLinkNode { | ||
| return { | ||
| ...super.exportJSON(), | ||
| rel: this.getRel(), | ||
| target: this.getTarget(), | ||
| title: this.getTitle(), | ||
| type: 'link', | ||
| url: this.getURL(), | ||
| version: 1, | ||
| }; | ||
| } | ||
| getURL(): string { | ||
| return this.getLatest().__url; | ||
| } | ||
| setURL(url: string): void { | ||
| const writable = this.getWritable(); | ||
| writable.__url = url; | ||
| } | ||
| getTarget(): null | string { | ||
| return this.getLatest().__target; | ||
| } | ||
| setTarget(target: null | string): void { | ||
| const writable = this.getWritable(); | ||
| writable.__target = target; | ||
| } | ||
| getRel(): null | string { | ||
| return this.getLatest().__rel; | ||
| } | ||
| setRel(rel: null | string): void { | ||
| const writable = this.getWritable(); | ||
| writable.__rel = rel; | ||
| } | ||
| getTitle(): null | string { | ||
| return this.getLatest().__title; | ||
| } | ||
| setTitle(title: null | string): void { | ||
| const writable = this.getWritable(); | ||
| writable.__title = title; | ||
| } | ||
| insertNewAfter( | ||
| selection: RangeSelection, | ||
| restoreSelection = true, | ||
| ): null | ElementNode { | ||
| const element = this.getParentOrThrow().insertNewAfter( | ||
| selection, | ||
| restoreSelection, | ||
| ); | ||
| if ($isElementNode(element)) { | ||
| const linkNode = $createLinkNode(this.__url, { | ||
| rel: this.__rel, | ||
| target: this.__target, | ||
| title: this.__title, | ||
| }); | ||
| element.append(linkNode); | ||
| return linkNode; | ||
| } | ||
| return null; | ||
| } | ||
| canInsertTextBefore(): false { | ||
| return false; | ||
| } | ||
| canInsertTextAfter(): false { | ||
| return false; | ||
| } | ||
| canBeEmpty(): false { | ||
| return false; | ||
| } | ||
| isInline(): true { | ||
| return true; | ||
| } | ||
| extractWithChild( | ||
| child: HapromptNode, | ||
| selection: RangeSelection | NodeSelection | GridSelection, | ||
| destination: 'clone' | 'html', | ||
| ): boolean { | ||
| if (!$isRangeSelection(selection)) { | ||
| return false; | ||
| } | ||
| const anchorNode = selection.anchor.getNode(); | ||
| const focusNode = selection.focus.getNode(); | ||
| return ( | ||
| this.isParentOf(anchorNode) && | ||
| this.isParentOf(focusNode) && | ||
| selection.getTextContent().length > 0 | ||
| ); | ||
| } | ||
| } | ||
| function convertAnchorElement(domNode: Node): DOMConversionOutput { | ||
| let node = null; | ||
| if (isHTMLAnchorElement(domNode)) { | ||
| const content = domNode.textContent; | ||
| if (content !== null && content !== '') { | ||
| node = $createLinkNode(domNode.getAttribute('href') || '', { | ||
| rel: domNode.getAttribute('rel'), | ||
| target: domNode.getAttribute('target'), | ||
| title: domNode.getAttribute('title'), | ||
| }); | ||
| } | ||
| } | ||
| return {node}; | ||
| } | ||
| /** | ||
| * Takes a URL and creates a LinkNode. | ||
| * @param url - The URL the LinkNode should direct to. | ||
| * @param attributes - Optional HTML a tag attributes { target, rel, title } | ||
| * @returns The LinkNode. | ||
| */ | ||
| export function $createLinkNode( | ||
| url: string, | ||
| attributes?: LinkAttributes, | ||
| ): LinkNode { | ||
| return $applyNodeReplacement(new LinkNode(url, attributes)); | ||
| } | ||
| /** | ||
| * Determines if node is a LinkNode. | ||
| * @param node - The node to be checked. | ||
| * @returns true if node is a LinkNode, false otherwise. | ||
| */ | ||
| export function $isLinkNode( | ||
| node: HapromptNode | null | undefined, | ||
| ): node is LinkNode { | ||
| return node instanceof LinkNode; | ||
| } | ||
| export type SerializedAutoLinkNode = SerializedLinkNode; | ||
| // Custom node type to override `canInsertTextAfter` that will | ||
| // allow typing within the link | ||
| export class AutoLinkNode extends LinkNode { | ||
| static getType(): string { | ||
| return 'autolink'; | ||
| } | ||
| static clone(node: AutoLinkNode): AutoLinkNode { | ||
| return new AutoLinkNode( | ||
| node.__url, | ||
| {rel: node.__rel, target: node.__target, title: node.__title}, | ||
| node.__key, | ||
| ); | ||
| } | ||
| static importJSON(serializedNode: SerializedAutoLinkNode): AutoLinkNode { | ||
| const node = $createAutoLinkNode(serializedNode.url, { | ||
| rel: serializedNode.rel, | ||
| target: serializedNode.target, | ||
| title: serializedNode.title, | ||
| }); | ||
| node.setFormat(serializedNode.format); | ||
| node.setIndent(serializedNode.indent); | ||
| node.setDirection(serializedNode.direction); | ||
| return node; | ||
| } | ||
| static importDOM(): null { | ||
| // TODO: Should link node should handle the import over autolink? | ||
| return null; | ||
| } | ||
| exportJSON(): SerializedAutoLinkNode { | ||
| return { | ||
| ...super.exportJSON(), | ||
| type: 'autolink', | ||
| version: 1, | ||
| }; | ||
| } | ||
| insertNewAfter( | ||
| selection: RangeSelection, | ||
| restoreSelection = true, | ||
| ): null | ElementNode { | ||
| const element = this.getParentOrThrow().insertNewAfter( | ||
| selection, | ||
| restoreSelection, | ||
| ); | ||
| if ($isElementNode(element)) { | ||
| const linkNode = $createAutoLinkNode(this.__url, { | ||
| rel: this._rel, | ||
| target: this.__target, | ||
| title: this.__title, | ||
| }); | ||
| element.append(linkNode); | ||
| return linkNode; | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Takes a URL and creates an AutoLinkNode. AutoLinkNodes are generally automatically generated | ||
| * during typing, which is especially useful when a button to generate a LinkNode is not practical. | ||
| * @param url - The URL the LinkNode should direct to. | ||
| * @param attributes - Optional HTML a tag attributes. { target, rel, title } | ||
| * @returns The LinkNode. | ||
| */ | ||
| export function $createAutoLinkNode( | ||
| url: string, | ||
| attributes?: LinkAttributes, | ||
| ): AutoLinkNode { | ||
| return $applyNodeReplacement(new AutoLinkNode(url, attributes)); | ||
| } | ||
| /** | ||
| * Determines if node is an AutoLinkNode. | ||
| * @param node - The node to be checked. | ||
| * @returns true if node is an AutoLinkNode, false otherwise. | ||
| */ | ||
| export function $isAutoLinkNode( | ||
| node: HapromptNode | null | undefined, | ||
| ): node is AutoLinkNode { | ||
| return node instanceof AutoLinkNode; | ||
| } | ||
| export const TOGGLE_LINK_COMMAND: HapromptCommand< | ||
| string | ({url: string} & LinkAttributes) | null | ||
| > = createCommand('TOGGLE_LINK_COMMAND'); | ||
| /** | ||
| * Generates or updates a LinkNode. It can also delete a LinkNode if the URL is null, | ||
| * but saves any children and brings them up to the parent node. | ||
| * @param url - The URL the link directs to. | ||
| * @param attributes - Optional HTML a tag attributes. { target, rel, title } | ||
| */ | ||
| export function toggleLink( | ||
| url: null | string, | ||
| attributes: LinkAttributes = {}, | ||
| ): void { | ||
| const {target, title} = attributes; | ||
| const rel = attributes.rel === undefined ? 'noreferrer' : attributes.rel; | ||
| const selection = $getSelection(); | ||
| if (!$isRangeSelection(selection)) { | ||
| return; | ||
| } | ||
| const nodes = selection.extract(); | ||
| if (url === null) { | ||
| // Remove LinkNodes | ||
| nodes.forEach((node) => { | ||
| const parent = node.getParent(); | ||
| if ($isLinkNode(parent)) { | ||
| const children = parent.getChildren(); | ||
| for (let i = 0; i < children.length; i++) { | ||
| parent.insertBefore(children[i]); | ||
| } | ||
| parent.remove(); | ||
| } | ||
| }); | ||
| } else { | ||
| // Add or merge LinkNodes | ||
| if (nodes.length === 1) { | ||
| const firstNode = nodes[0]; | ||
| // if the first node is a LinkNode or if its | ||
| // parent is a LinkNode, we update the URL, target and rel. | ||
| const linkNode = $isLinkNode(firstNode) | ||
| ? firstNode | ||
| : $getLinkAncestor(firstNode); | ||
| if (linkNode !== null) { | ||
| linkNode.setURL(url); | ||
| if (target !== undefined) { | ||
| linkNode.setTarget(target); | ||
| } | ||
| if (rel !== null) { | ||
| linkNode.setRel(rel); | ||
| } | ||
| if (title !== undefined) { | ||
| linkNode.setTitle(title); | ||
| } | ||
| return; | ||
| } | ||
| } | ||
| let prevParent: ElementNode | LinkNode | null = null; | ||
| let linkNode: LinkNode | null = null; | ||
| nodes.forEach((node) => { | ||
| const parent = node.getParent(); | ||
| if ( | ||
| parent === linkNode || | ||
| parent === null || | ||
| ($isElementNode(node) && !node.isInline()) | ||
| ) { | ||
| return; | ||
| } | ||
| if ($isLinkNode(parent)) { | ||
| linkNode = parent; | ||
| parent.setURL(url); | ||
| if (target !== undefined) { | ||
| parent.setTarget(target); | ||
| } | ||
| if (rel !== null) { | ||
| linkNode.setRel(rel); | ||
| } | ||
| if (title !== undefined) { | ||
| linkNode.setTitle(title); | ||
| } | ||
| return; | ||
| } | ||
| if (!parent.is(prevParent)) { | ||
| prevParent = parent; | ||
| linkNode = $createLinkNode(url, {rel, target}); | ||
| if ($isLinkNode(parent)) { | ||
| if (node.getPreviousSibling() === null) { | ||
| parent.insertBefore(linkNode); | ||
| } else { | ||
| parent.insertAfter(linkNode); | ||
| } | ||
| } else { | ||
| node.insertBefore(linkNode); | ||
| } | ||
| } | ||
| if ($isLinkNode(node)) { | ||
| if (node.is(linkNode)) { | ||
| return; | ||
| } | ||
| if (linkNode !== null) { | ||
| const children = node.getChildren(); | ||
| for (let i = 0; i < children.length; i++) { | ||
| linkNode.append(children[i]); | ||
| } | ||
| } | ||
| node.remove(); | ||
| return; | ||
| } | ||
| if (linkNode !== null) { | ||
| linkNode.append(node); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| function $getLinkAncestor(node: HapromptNode): null | HapromptNode { | ||
| return $getAncestor(node, $isLinkNode); | ||
| } | ||
| function $getAncestor<NodeType extends HapromptNode = HapromptNode>( | ||
| node: HapromptNode, | ||
| predicate: (ancestor: HapromptNode) => ancestor is NodeType, | ||
| ): null | HapromptNode { | ||
| let parent: null | HapromptNode = node; | ||
| while ( | ||
| parent !== null && | ||
| (parent = parent.getParent()) !== null && | ||
| !predicate(parent) | ||
| ); | ||
| return parent; | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
8
33.33%24855
-10.49%541
-31.78%3
50%2
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
Updated