Skip to content

Commit 5f68dcf

Browse files
authored
Merge pull request #40 from typecode/issue-39
#39 - Link flyout position refinment
2 parents fb53837 + 31c0c16 commit 5f68dcf

File tree

7 files changed

+117
-44
lines changed

7 files changed

+117
-44
lines changed

src/scripts/modules/LinkFormatter.js

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* mediator.exec('format:link'); // Remove link if already a link, otherwise show link toolbar flyout.
1212
* mediator.request('format:link:active'); // returns true if selection is in or wraps a link.
1313
*/
14-
14+
1515
import Module from '../core/Module';
1616
import commands from '../utils/commands';
1717
import DOM from '../utils/DOM';
@@ -20,12 +20,12 @@ import inputFormTemplate from '../../templates/inputForm.html';
2020
import linkDisplayTemplate from '../../templates/linkDisplay.html';
2121

2222
import inputFormStyles from '../../styles/inputForm.scss';
23+
import linkDisplayStyles from '../../styles/linkDisplay.scss';
2324

2425
const LinkFormatter = Module({
2526
name: 'LinkFormatter',
2627
props: {
2728
urlRegex: /[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/,
28-
styles: null,
2929
currentAnchor: null,
3030
active: false,
3131
hasMouse: false,
@@ -66,8 +66,8 @@ const LinkFormatter = Module({
6666
},
6767

6868
appendStyles () {
69-
const { props } = this;
70-
props.styles = DOM.addStyles(inputFormStyles);
69+
DOM.addStyles(inputFormStyles);
70+
DOM.addStyles(linkDisplayStyles);
7171
},
7272

7373
formatLink () {
@@ -121,6 +121,7 @@ const LinkFormatter = Module({
121121

122122
hideFlyout () {
123123
const { props } = this;
124+
124125
props.hideTimeout = setTimeout(() => {
125126
if (!this.isActive() && props.hasRendered) {
126127
this.destroy();
@@ -151,12 +152,12 @@ const LinkFormatter = Module({
151152

152153
compileLinkDisplay (data) {
153154
const wrapperEl = document.createElement('div');
154-
let inputFormHTML = linkDisplayTemplate(data);
155+
let linkDisplayHTML = linkDisplayTemplate(data);
155156

156-
if (typeof inputFormHTML === 'string') {
157-
wrapperEl.innerHTML = inputFormHTML;
157+
if (typeof linkDisplayHTML === 'string') {
158+
wrapperEl.innerHTML = linkDisplayHTML;
158159
} else {
159-
wrapperEl.appendChild(inputFormHTML[0]);
160+
wrapperEl.appendChild(linkDisplayHTML[0]);
160161
}
161162

162163
return wrapperEl.childNodes[0];
@@ -180,15 +181,19 @@ const LinkFormatter = Module({
180181

181182
positionFlyout (opts) {
182183
const { mediator, props } = this;
183-
const { initialEvent, targetEl } = props;
184-
let targetBounds, elStyles, elLineHeight, lineCount, lineStepHeight;
184+
const { targetEl } = props;
185+
let targetBounds;
185186

186187
if (targetEl) {
187188
targetBounds = targetEl.getBoundingClientRect();
188-
elStyles = window.getComputedStyle(targetEl);
189-
elLineHeight = parseInt(elStyles.getPropertyValue('line-height'), 10);
190-
lineCount = Math.ceil(targetBounds.height / elLineHeight);
191-
lineStepHeight = targetBounds.height / lineCount;
189+
190+
// See reason below - Fred
191+
// elStyles = window.getComputedStyle(targetEl);
192+
// elLineHeight = elStyles.getPropertyValue('line-height');
193+
// elLineHeight = elLineHeight === 'normal' ? elStyles.getPropertyValue('font-size') : elStyles.getPropertyValue('line-height');
194+
// elLineHeight = parseInt(elLineHeight, 10);
195+
// lineCount = Math.ceil(targetBounds.height / elLineHeight);
196+
// lineStepHeight = targetBounds.height / lineCount;
192197
} else {
193198
targetBounds = mediator.get('selection:bounds');
194199
}
@@ -197,21 +202,29 @@ const LinkFormatter = Module({
197202
const scrollOffset = DOM.getScrollOffset();
198203
let docRelTop, docRelCenter;
199204

200-
if (initialEvent) {
201-
const topDiff = initialEvent.clientY - targetBounds.top;
202-
203-
docRelTop = initialEvent.clientY;
204-
docRelCenter = initialEvent.clientX;
205-
206-
if (opts.flyoutPlacement === 'below') {
207-
docRelTop = targetBounds.top + (lineStepHeight * Math.ceil(topDiff / lineStepHeight));
208-
} else {
209-
docRelTop = targetBounds.top + (lineStepHeight * Math.floor(topDiff / lineStepHeight));
210-
}
211-
} else {
212-
docRelTop = (opts.flyoutPlacement === 'below' ? targetBounds.bottom : targetBounds.top);
213-
docRelCenter = targetBounds.width / 2 + targetBounds.left + scrollOffset.x;
214-
}
205+
// Commenting this out because it is trying to do something smart (position
206+
// the flyout close to the user's cursor when they hover over a link)
207+
// but isn't particularly stable. And the alternative of positioning it underneath
208+
// is acceptable and stable. Leaving this here in case the alternative
209+
// proves to be a pain.
210+
// - Fred
211+
//
212+
// const { initialEvent } = props;
213+
// if (false && initialEvent) {
214+
// const topDiff = initialEvent.clientY - targetBounds.top;
215+
//
216+
// docRelTop = initialEvent.clientY;
217+
// docRelCenter = initialEvent.clientX;
218+
//
219+
// if (opts.flyoutPlacement === 'below') {
220+
// docRelTop = targetBounds.top + (lineStepHeight * Math.ceil(topDiff / lineStepHeight));
221+
// } else {
222+
// docRelTop = targetBounds.top + (lineStepHeight * Math.floor(topDiff / lineStepHeight));
223+
// }
224+
// } else {
225+
docRelTop = (opts.flyoutPlacement === 'below' ? targetBounds.bottom : targetBounds.top);
226+
docRelCenter = targetBounds.width / 2 + targetBounds.left + scrollOffset.x;
227+
// }
215228

216229
docRelTop += scrollOffset.y;
217230

@@ -259,7 +272,9 @@ const LinkFormatter = Module({
259272

260273
handleClick (evnt) {
261274
const { mediator, props } = this;
262-
if (evnt.target.nodeName === 'A') {
275+
const anchor = DOM.getClosest(evnt.target, 'a');
276+
277+
if (anchor && anchor.classList.contains('typester-link-edit')) {
263278
evnt.preventDefault();
264279
mediator.exec('selection:wrap:element', props.currentAnchor, { silent: true });
265280
this.showLinkFormFlyout({ value: props.currentAnchor.href });
@@ -366,14 +381,24 @@ const LinkFormatter = Module({
366381

367382
destroy () {
368383
const { props, mediator } = this;
384+
const selectionAnchorNode = mediator.get('selection:anchornode');
385+
369386
if (props.flyout) {
370387
this.unbindInput();
371388
props.flyout.remove();
372389
}
390+
373391
props.showing = null;
374392
props.hasMouse = false;
375393
props.hasRendered = null;
394+
props.targetEl = null;
395+
396+
if (selectionAnchorNode) {
397+
selectionAnchorNode.parentElement.normalize();
398+
}
399+
376400
mediator.exec('selection:select:remove:pseudo');
401+
377402
}
378403
}
379404
});

src/scripts/modules/Selection.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
* 'selection:in:or:contains': 'inOrContains',
2525
* 'selection:range:coordinates': 'rangeCoordinates',
2626
* 'selection:contains:node': 'containsNode',
27-
* 'selection:spans:multiple:blocks': 'spansMultipleBlocks'
27+
* 'selection:spans:multiple:blocks': 'spansMultipleBlocks',
28+
* 'selection:pseudo': 'getPseudo'
2829
* },
2930
*
3031
* commands: {
@@ -86,7 +87,8 @@ const Selection = Module({
8687
'selection:in:or:contains': 'inOrContains',
8788
'selection:range:coordinates': 'rangeCoordinates',
8889
'selection:contains:node': 'containsNode',
89-
'selection:spans:multiple:blocks': 'spansMultipleBlocks'
90+
'selection:spans:multiple:blocks': 'spansMultipleBlocks',
91+
'selection:pseudo': 'getPseudo'
9092
},
9193

9294
commands: {
@@ -450,6 +452,11 @@ const Selection = Module({
450452
}
451453
},
452454

455+
getPseudo () {
456+
const { props } = this;
457+
return props.pseudoSelection;
458+
},
459+
453460
removePseudo () {
454461
const { props } = this;
455462
let unwrappedNodes = [];

src/scripts/utils/DOM.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,14 @@ const DOM = {
6666
return null;
6767
}
6868

69-
while (node.nodeType !== Node.ELEMENT_NODE) {
69+
while (node && node.nodeType !== Node.ELEMENT_NODE) {
7070
node = node.parentNode;
7171
}
7272

73+
if (!node) {
74+
return null;
75+
}
76+
7377
switch (checkType) {
7478
case 'attribute':
7579
attrName = selector.match(/\[(.*?)\]/)[1];

src/styles/inputForm.scss

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@
4040
}
4141
}
4242

43-
.typester-link-display {
44-
a {
45-
display: block;
46-
cursor: pointer;
47-
line-height: 20px;
48-
padding: 10px;
49-
}
43+
.typester-pseudo-selection {
44+
background: #CCC;
5045
}

src/styles/linkDisplay.scss

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.typester-link-display {
2+
display: flex;
3+
4+
a {
5+
display: block;
6+
cursor: pointer;
7+
line-height: 20px;
8+
padding: 10px;
9+
}
10+
11+
a[href] {
12+
text-decoration: none;
13+
14+
&:hover {
15+
text-decoration: underline;
16+
}
17+
}
18+
19+
.typester-link-edit {
20+
display: block;
21+
height: 20px;
22+
text-align: center;
23+
24+
svg {
25+
display: block;
26+
width: 20px;
27+
height: 20px;
28+
fill: #FFF;
29+
}
30+
31+
&:hover {
32+
background: rgb(0, 174, 239);
33+
}
34+
}
35+
}

src/templates/linkDisplay.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
<div class='typester-link-display'>
2-
<a>{{ href }}</a>
2+
<a href='{{ href }}' target='_blank'>{{ href }}</a>
3+
<a class='typester-link-edit'>
4+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768">
5+
<path d="M725 189.667l-71.5 71.501-146.666-146.666 71.501-71.501c14.666-14.666 40.334-14.666 55 0l91.666 91.666c14.666 14.666 14.666 40.334 0 55.001zM31.999 589.334l432.667-432.667 146.666 146.666-432.667 432.667h-146.666v-146.666z"></path>
6+
</svg>
7+
</a>
38
</div>

test/server/html/index.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,19 @@ <h1>Typester test server</h1>
7777
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.
7878
</p>
7979
<p>
80-
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.
80+
Praesent sapien massa, <a href='https://typecode.com'>convallis a pellentesque nec</a>, 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.
8181
</p>
8282
<p>
83-
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.
83+
<a href='https://typecode.com'>
84+
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.
85+
</a>
8486
</p>
8587
<p>
8688
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.
8789
</p>
8890
<ul>
8991
<li>
90-
Sed porttitor lectus nibh.
92+
<a href='https://typecode.com'>Sed porttitor lectus nibh.</a>
9193
</li>
9294
<li>
9395
Donec rutrum congue leo eget malesuada.

0 commit comments

Comments
 (0)