From 31c0c1680372d78e4b91954c4d4e13616117331b Mon Sep 17 00:00:00 2001
From: Fred Every
Date: Fri, 29 Jun 2018 16:24:06 +0200
Subject: [PATCH] #39 - Link flyout position refinment + overall link editing
refinement
---
src/scripts/modules/LinkFormatter.js | 85 ++++++++++++++++++----------
src/scripts/modules/Selection.js | 11 +++-
src/scripts/utils/DOM.js | 6 +-
src/styles/inputForm.scss | 9 +--
src/styles/linkDisplay.scss | 35 ++++++++++++
src/templates/linkDisplay.html | 7 ++-
test/server/html/index.html | 8 ++-
7 files changed, 117 insertions(+), 44 deletions(-)
create mode 100644 src/styles/linkDisplay.scss
diff --git a/src/scripts/modules/LinkFormatter.js b/src/scripts/modules/LinkFormatter.js
index d29020c..a67a7b5 100644
--- a/src/scripts/modules/LinkFormatter.js
+++ b/src/scripts/modules/LinkFormatter.js
@@ -11,7 +11,7 @@
* mediator.exec('format:link'); // Remove link if already a link, otherwise show link toolbar flyout.
* mediator.request('format:link:active'); // returns true if selection is in or wraps a link.
*/
-
+
import Module from '../core/Module';
import commands from '../utils/commands';
import DOM from '../utils/DOM';
@@ -20,12 +20,12 @@ import inputFormTemplate from '../../templates/inputForm.html';
import linkDisplayTemplate from '../../templates/linkDisplay.html';
import inputFormStyles from '../../styles/inputForm.scss';
+import linkDisplayStyles from '../../styles/linkDisplay.scss';
const LinkFormatter = Module({
name: 'LinkFormatter',
props: {
urlRegex: /[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/,
- styles: null,
currentAnchor: null,
active: false,
hasMouse: false,
@@ -66,8 +66,8 @@ const LinkFormatter = Module({
},
appendStyles () {
- const { props } = this;
- props.styles = DOM.addStyles(inputFormStyles);
+ DOM.addStyles(inputFormStyles);
+ DOM.addStyles(linkDisplayStyles);
},
formatLink () {
@@ -121,6 +121,7 @@ const LinkFormatter = Module({
hideFlyout () {
const { props } = this;
+
props.hideTimeout = setTimeout(() => {
if (!this.isActive() && props.hasRendered) {
this.destroy();
@@ -151,12 +152,12 @@ const LinkFormatter = Module({
compileLinkDisplay (data) {
const wrapperEl = document.createElement('div');
- let inputFormHTML = linkDisplayTemplate(data);
+ let linkDisplayHTML = linkDisplayTemplate(data);
- if (typeof inputFormHTML === 'string') {
- wrapperEl.innerHTML = inputFormHTML;
+ if (typeof linkDisplayHTML === 'string') {
+ wrapperEl.innerHTML = linkDisplayHTML;
} else {
- wrapperEl.appendChild(inputFormHTML[0]);
+ wrapperEl.appendChild(linkDisplayHTML[0]);
}
return wrapperEl.childNodes[0];
@@ -180,15 +181,19 @@ const LinkFormatter = Module({
positionFlyout (opts) {
const { mediator, props } = this;
- const { initialEvent, targetEl } = props;
- let targetBounds, elStyles, elLineHeight, lineCount, lineStepHeight;
+ const { targetEl } = props;
+ let targetBounds;
if (targetEl) {
targetBounds = targetEl.getBoundingClientRect();
- elStyles = window.getComputedStyle(targetEl);
- elLineHeight = parseInt(elStyles.getPropertyValue('line-height'), 10);
- lineCount = Math.ceil(targetBounds.height / elLineHeight);
- lineStepHeight = targetBounds.height / lineCount;
+
+ // See reason below - Fred
+ // elStyles = window.getComputedStyle(targetEl);
+ // elLineHeight = elStyles.getPropertyValue('line-height');
+ // elLineHeight = elLineHeight === 'normal' ? elStyles.getPropertyValue('font-size') : elStyles.getPropertyValue('line-height');
+ // elLineHeight = parseInt(elLineHeight, 10);
+ // lineCount = Math.ceil(targetBounds.height / elLineHeight);
+ // lineStepHeight = targetBounds.height / lineCount;
} else {
targetBounds = mediator.get('selection:bounds');
}
@@ -197,21 +202,29 @@ const LinkFormatter = Module({
const scrollOffset = DOM.getScrollOffset();
let docRelTop, docRelCenter;
- if (initialEvent) {
- const topDiff = initialEvent.clientY - targetBounds.top;
-
- docRelTop = initialEvent.clientY;
- docRelCenter = initialEvent.clientX;
-
- if (opts.flyoutPlacement === 'below') {
- docRelTop = targetBounds.top + (lineStepHeight * Math.ceil(topDiff / lineStepHeight));
- } else {
- docRelTop = targetBounds.top + (lineStepHeight * Math.floor(topDiff / lineStepHeight));
- }
- } else {
- docRelTop = (opts.flyoutPlacement === 'below' ? targetBounds.bottom : targetBounds.top);
- docRelCenter = targetBounds.width / 2 + targetBounds.left + scrollOffset.x;
- }
+ // Commenting this out because it is trying to do something smart (position
+ // the flyout close to the user's cursor when they hover over a link)
+ // but isn't particularly stable. And the alternative of positioning it underneath
+ // is acceptable and stable. Leaving this here in case the alternative
+ // proves to be a pain.
+ // - Fred
+ //
+ // const { initialEvent } = props;
+ // if (false && initialEvent) {
+ // const topDiff = initialEvent.clientY - targetBounds.top;
+ //
+ // docRelTop = initialEvent.clientY;
+ // docRelCenter = initialEvent.clientX;
+ //
+ // if (opts.flyoutPlacement === 'below') {
+ // docRelTop = targetBounds.top + (lineStepHeight * Math.ceil(topDiff / lineStepHeight));
+ // } else {
+ // docRelTop = targetBounds.top + (lineStepHeight * Math.floor(topDiff / lineStepHeight));
+ // }
+ // } else {
+ docRelTop = (opts.flyoutPlacement === 'below' ? targetBounds.bottom : targetBounds.top);
+ docRelCenter = targetBounds.width / 2 + targetBounds.left + scrollOffset.x;
+ // }
docRelTop += scrollOffset.y;
@@ -259,7 +272,9 @@ const LinkFormatter = Module({
handleClick (evnt) {
const { mediator, props } = this;
- if (evnt.target.nodeName === 'A') {
+ const anchor = DOM.getClosest(evnt.target, 'a');
+
+ if (anchor && anchor.classList.contains('typester-link-edit')) {
evnt.preventDefault();
mediator.exec('selection:wrap:element', props.currentAnchor, { silent: true });
this.showLinkFormFlyout({ value: props.currentAnchor.href });
@@ -366,14 +381,24 @@ const LinkFormatter = Module({
destroy () {
const { props, mediator } = this;
+ const selectionAnchorNode = mediator.get('selection:anchornode');
+
if (props.flyout) {
this.unbindInput();
props.flyout.remove();
}
+
props.showing = null;
props.hasMouse = false;
props.hasRendered = null;
+ props.targetEl = null;
+
+ if (selectionAnchorNode) {
+ selectionAnchorNode.parentElement.normalize();
+ }
+
mediator.exec('selection:select:remove:pseudo');
+
}
}
});
diff --git a/src/scripts/modules/Selection.js b/src/scripts/modules/Selection.js
index f95adaf..a961538 100644
--- a/src/scripts/modules/Selection.js
+++ b/src/scripts/modules/Selection.js
@@ -24,7 +24,8 @@
* 'selection:in:or:contains': 'inOrContains',
* 'selection:range:coordinates': 'rangeCoordinates',
* 'selection:contains:node': 'containsNode',
- * 'selection:spans:multiple:blocks': 'spansMultipleBlocks'
+ * 'selection:spans:multiple:blocks': 'spansMultipleBlocks',
+ * 'selection:pseudo': 'getPseudo'
* },
*
* commands: {
@@ -86,7 +87,8 @@ const Selection = Module({
'selection:in:or:contains': 'inOrContains',
'selection:range:coordinates': 'rangeCoordinates',
'selection:contains:node': 'containsNode',
- 'selection:spans:multiple:blocks': 'spansMultipleBlocks'
+ 'selection:spans:multiple:blocks': 'spansMultipleBlocks',
+ 'selection:pseudo': 'getPseudo'
},
commands: {
@@ -450,6 +452,11 @@ const Selection = Module({
}
},
+ getPseudo () {
+ const { props } = this;
+ return props.pseudoSelection;
+ },
+
removePseudo () {
const { props } = this;
let unwrappedNodes = [];
diff --git a/src/scripts/utils/DOM.js b/src/scripts/utils/DOM.js
index 10b42ea..f6b4769 100644
--- a/src/scripts/utils/DOM.js
+++ b/src/scripts/utils/DOM.js
@@ -66,10 +66,14 @@ const DOM = {
return null;
}
- while (node.nodeType !== Node.ELEMENT_NODE) {
+ while (node && node.nodeType !== Node.ELEMENT_NODE) {
node = node.parentNode;
}
+ if (!node) {
+ return null;
+ }
+
switch (checkType) {
case 'attribute':
attrName = selector.match(/\[(.*?)\]/)[1];
diff --git a/src/styles/inputForm.scss b/src/styles/inputForm.scss
index bc3e3de..7bcd7ec 100644
--- a/src/styles/inputForm.scss
+++ b/src/styles/inputForm.scss
@@ -40,11 +40,6 @@
}
}
-.typester-link-display {
- a {
- display: block;
- cursor: pointer;
- line-height: 20px;
- padding: 10px;
- }
+.typester-pseudo-selection {
+ background: #CCC;
}
diff --git a/src/styles/linkDisplay.scss b/src/styles/linkDisplay.scss
new file mode 100644
index 0000000..8a055c8
--- /dev/null
+++ b/src/styles/linkDisplay.scss
@@ -0,0 +1,35 @@
+.typester-link-display {
+ display: flex;
+
+ a {
+ display: block;
+ cursor: pointer;
+ line-height: 20px;
+ padding: 10px;
+ }
+
+ a[href] {
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .typester-link-edit {
+ display: block;
+ height: 20px;
+ text-align: center;
+
+ svg {
+ display: block;
+ width: 20px;
+ height: 20px;
+ fill: #FFF;
+ }
+
+ &:hover {
+ background: rgb(0, 174, 239);
+ }
+ }
+}
diff --git a/src/templates/linkDisplay.html b/src/templates/linkDisplay.html
index a0bca0f..b8df200 100644
--- a/src/templates/linkDisplay.html
+++ b/src/templates/linkDisplay.html
@@ -1,3 +1,8 @@
diff --git a/test/server/html/index.html b/test/server/html/index.html
index 6a56948..97fb31a 100644
--- a/test/server/html/index.html
+++ b/test/server/html/index.html
@@ -77,17 +77,19 @@ Typester test server
Sed porttitor lectus nibh. Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Pellentesque in ipsum id orci porta dapibus. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec velit neque, auctor sit amet aliquam vel, ullamcorper sit amet ligula.
- Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Vivamus suscipit tortor eget felis porttitor volutpat. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Vivamus suscipit tortor eget felis porttitor volutpat. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Cras ultricies ligula sed magna dictum porta. Donec sollicitudin molestie malesuada. Pellentesque in ipsum id orci porta dapibus. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi.
+
+ Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Cras ultricies ligula sed magna dictum porta. Donec sollicitudin molestie malesuada. Pellentesque in ipsum id orci porta dapibus. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi.
+
Nulla quis lorem ut libero malesuada feugiat. Donec sollicitudin molestie malesuada. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Sed porttitor lectus nibh. Donec sollicitudin molestie malesuada.