Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@convertkit/selection-toolbar

Package Overview
Dependencies
Maintainers
3
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@convertkit/selection-toolbar - npm Package Compare versions

Comparing version 0.1.8 to 0.1.9

src/use-selection.js

4

package.json
{
"name": "@convertkit/selection-toolbar",
"version": "0.1.8",
"version": "0.1.9",
"description": "A toolbar for the ConvertKit Editor that hovers over the selection.",

@@ -35,3 +35,3 @@ "main": "dist/index.js",

},
"gitHead": "251b4984e9f6e5558656f507400c994ce15553b9"
"gitHead": "8db86681a3d9ac14d4a3954a492acb3e63bbf6f5"
}

@@ -1,176 +0,111 @@

import React, { Component } from "react";
import PropTypes from "prop-types";
import React, { useRef, useState, useCallback } from "react";
import classNames from "classnames";
import { isTextSelected } from "./utils";
import PositionElement from "../../../shared/components/position-element";
import SelectionRect from "./selection-rect";
import {
isTextSelected,
useDebounce,
useTransitionElement,
useEventListener
} from "./utils";
import { useSelectionPosition } from "./use-selection";
import Toolbar from "./toolbar";
import { trailingDebounce } from "./utils";
import "./selection-toolbar.css";
import isHotkey from "is-hotkey";
const wrapRect = rect => ({ getBoundingClientRect: () => rect });
function SelectionToolbar({ editor, buttons, exclude, style }) {
const toolbar = useRef();
const transition = (element, fn, callback) => {
function listener() {
callback();
element.removeEventListener("transitionend", listener);
}
element.addEventListener("transitionend", listener);
fn();
};
const [transitioning, startTransition] = useTransitionElement(
toolbar.current
);
class SelectionToolbar extends Component {
static propTypes = {
buttons: PropTypes.array.isRequired,
exclude: PropTypes.array,
styles: PropTypes.object.isRequired
const [visible, setVisible] = useState(false);
const [menu, setMenu] = useState(null);
const openMenu = menu => {
setMenu(menu);
};
static defaultProps = {
buttons: [],
styles: {}
const closeMenu = () => {
setMenu(null);
};
state = { visible: false, menu: null };
toolbar = React.createRef();
const { styles, arrowStyles } = useSelectionPosition(toolbar.current, {
placement: "top",
debounce: menu ? -1 : visible ? 300 : 100
});
componentDidMount() {
document.addEventListener("selectionchange", this.debouncedUpdate);
document.addEventListener("keydown", this.handleKeyDown);
}
componentWillUnmount() {
document.removeEventListener("selectionchange", this.debouncedUpdate);
}
update = () => {
const visible = this.state.visible;
const menuOpen = this.state.menu != undefined;
const textSelected = isTextSelected(this.props.editor.value);
const shouldShow = textSelected && !visible;
const shouldHide = !textSelected && !menuOpen && visible;
if (shouldShow) return this.show();
if (shouldHide) return this.hide();
const show = () => {
startTransition();
setVisible(true);
};
debouncedUpdate = trailingDebounce(this.update, 200);
show = () => {
if (!this.toolbar.current) return;
transition(
this.toolbar.current,
() => {
this.setState({ visible: true, transitioning: true });
},
this.transitionEnd
);
const hide = () => {
startTransition();
setVisible(false);
closeMenu();
};
hide = () => {
if (!this.toolbar.current) return;
transition(
this.toolbar.current,
() => {
this.setState({ visible: false, transitioning: true });
},
() => {
this.transitionEnd();
this.closeMenu();
}
);
};
const textSelected = isTextSelected(editor.value);
transitionEnd = () => {
this.setState({ transitioning: false });
};
const [handleSelectionChange] = useDebounce(
() => {
const shouldShow = textSelected && !visible;
const shouldHide = !textSelected && !menu;
handleMouseDown = e => {
e.preventDefault();
e.stopPropagation();
};
if (shouldShow) return show();
if (shouldHide) return hide();
},
200,
[textSelected, visible, menu]
);
handleKeyDown = e => {
e.stopPropagation();
const handleKeyDown = event => {
event.stopPropagation();
if (!this.state.visible) return;
if (!visible) return;
this.props.buttons.find((button, index) => {
if (!button.shortcut) return;
buttons.find((button, index) => {
if (!button.shortcut) return false;
if (isHotkey(button.shortcut, e)) {
this.setState({ menu: index });
if (isHotkey(button.shortcut, event)) {
openMenu(index);
return true;
}
return false;
});
};
openMenu = menu => {
this.setState({ menu });
};
useEventListener(document, "selectionchange", handleSelectionChange);
useEventListener(window, "resize", handleSelectionChange);
useEventListener(document, "keydown", handleKeyDown);
closeMenu = () => {
this.setState({ menu: null });
};
const className = classNames("convertkit-editor-selection-toolbar", {
visible,
active: isTextSelected(editor.value),
transitioning
});
excludedBlock = () =>
this.props.editor.value.blocks.some(block =>
this.props.exclude.includes(block.type)
);
render() {
const { buttons, editor, exclude, styles } = this.props;
const { menu, transitioning, visible } = this.state;
const { button: buttonStyle, ...toolbarStyle } = styles;
if (this.excludedBlock()) {
return null;
}
const className = classNames("convertkit-editor-selection-toolbar", {
visible,
transitioning
});
const menuOpen = menu != undefined;
const freezePosition = transitioning || menuOpen;
const debounce = freezePosition ? -1 : visible ? 201 : 0;
return (
<SelectionRect debounce={debounce}>
{rect => (
<PositionElement
active={this.state.visible}
target={wrapRect(rect)}
element={this.toolbar.current}
alignX="center"
alignY="top"
>
{({ bottom, left }) => (
<Toolbar
ref={this.toolbar}
buttons={buttons}
editor={editor}
menu={{
active: menu != null,
index: menu,
close: this.hide,
open: this.openMenu
}}
className={className}
style={{
bottom: bottom + 8,
left,
...toolbarStyle
}}
/>
)}
</PositionElement>
)}
</SelectionRect>
);
}
return (
<Toolbar
ref={toolbar}
className={className}
buttons={buttons}
editor={editor}
menu={{
active: menu != null,
index: menu,
close: hide,
open: openMenu
}}
style={{
...styles
}}
arrowStyle={arrowStyles}
/>
);
}
export default SelectionToolbar;

@@ -6,3 +6,3 @@ import React from "react";

export default React.forwardRef(function Toolbar(
{ buttons, editor, menu, className, style },
{ arrow, buttons, editor, menu, className, style, arrowStyle },
ref

@@ -36,4 +36,9 @@ ) {

</div>
<div
ref={arrow}
style={{ left: arrowStyle.left || 0 }}
className="selection-toolbar-arrow"
/>
</div>
);
});

@@ -1,22 +0,124 @@

export const transition = (element, fn, callback) => {
function listener() {
callback();
element.removeEventListener("transitionend", listener);
}
element.addEventListener("transitionend", listener);
fn();
import { useCallback, useEffect, useRef, useState } from "react";
/**
* Calls the function `debounce` milliseconds after the last time the function
* is called (trailing debounce). Any change to deps will reset the timer, not
* calling the function.
*
* @param {function} callback
* @param {integer} debounce
* @param {array} deps
*/
export const useDebounce = (callback, debounce, deps) => {
const timeout = useRef();
const cb = useRef();
useEffect(() => {
cb.current = callback;
}, [callback]);
const cancel = useCallback(() => {
clearTimeout(timeout.current);
}, [timeout.current]);
useEffect(() => {
cancel();
}, [debounce]);
const debounced = useCallback(
(...args) => {
cancel();
timeout.current = setTimeout(() => {
cb.current(...args);
}, debounce);
},
[debounce, ...deps]
);
return [debounced];
};
export const trailingDebounce = (fn, time) => {
let timeout;
return function() {
const args = arguments;
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(context, args);
}, time);
/**
* Used to declaratively determine if an element is in a CSS transition.
*
* Example:
* CSS
* .block {
* left: 0px;
* transition: left 300ms ease-in-out;
* }
*
* .block.transitioning {
* background: red
* }
*
* function Demo() {
* const [left, setLeft] = useState(0)
* const [transitioning, start] = useTransitionElement(elementReft)
*
* const moveLeft = () => {
* start()
* setLeft(left + 100)
* }
*
* let className = "block"
* if (transitioning) className += " transitioning"
*
* return (
* <button className="transitioning" style={{ left }} onClick={moveLeft}>
* Move Left
* </button>
* )
* }
*
* In this example, the "transitioning" class will be on the element after
* clicking start for the transition period specified in the CSS, or 500ms,
* whichever is shorter.
*/
export const useTransitionElement = (element, timeout = 500) => {
const [transitioning, setTransitioning] = useState(false);
const start = () => {
setTransitioning(true);
setTimeout(() => {
setTransitioning(false);
}, timeout);
};
const end = () => {
setTransitioning(false);
};
useEffect(() => {
if (!element) return;
element.addEventListener("transitionend", end);
return () => {
element.removeEventListener("transitionend", end);
};
}, [element]);
if (!element) return [false, start];
return [transitioning, start];
};
/**
* A convenience method for adding and cleaning up event listeners.
*
* @param {DOMElement} target
* @param {string} event
* @param {function} callback
*/
export const useEventListener = (target, event, callback) => {
return useEffect(() => {
target.addEventListener(event, callback);
return () => {
target.removeEventListener(event, callback);
};
}, [callback]);
};
export const isTextSelected = value => {

@@ -23,0 +125,0 @@ const { fragment, selection } = value;

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc