Skip to content

Commit 9c8c31f

Browse files
author
Clemens Prerovsky
committed
decoupled aloha ui links implementation from the rest of the ui.
1 parent 1c23a2e commit 9c8c31f

File tree

3 files changed

+337
-303
lines changed

3 files changed

+337
-303
lines changed

demo/aloha-ui/aloha-ui-links.js

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
(function (aloha) {
2+
'use strict';
3+
4+
var $$ = aloha.editor.ui.$$;
5+
var Fn = aloha.fn;
6+
var Dom = aloha.dom;
7+
var Keys = aloha.keys;
8+
var Maps = aloha.maps;
9+
var Editor = aloha.editor;
10+
var Arrays = aloha.arrays;
11+
var Editing = aloha.editing;
12+
var Selections = aloha.selections;
13+
var Boundaries = aloha.boundaries;
14+
var Traversing = aloha.traversing;
15+
16+
/**
17+
* Positions the given toolbar element to point to the anchor element in the
18+
* document.
19+
*
20+
* @param {!Element} toolbar
21+
* @param {!Element} anchor
22+
*/
23+
function positionToolbar(toolbar, anchor) {
24+
var box = aloha.carets.box(Boundaries.range(
25+
Boundaries.create(anchor, 0),
26+
Boundaries.create(anchor, 1)
27+
));
28+
var center = Math.round(box.left + (box.width / 2));
29+
var win = Dom.documentWindow(anchor.ownerDocument);
30+
var windowWidth = win.innerWidth;
31+
var toolbarWidth = parseInt(Dom.getComputedStyle(toolbar, 'width'), 10);
32+
var buffer = 10;
33+
var xMin = buffer;
34+
var xMax = (windowWidth - toolbarWidth) - buffer;
35+
var x = Math.min(xMax, Math.max(xMin, center - (toolbarWidth / 2)));
36+
var y = box.top + box.height + buffer;
37+
Dom.setStyle(toolbar, 'left', x + 'px');
38+
Dom.setStyle(toolbar, 'top', y + 'px');
39+
var arrow = toolbar.querySelector('.aloha-arrow-up');
40+
var arrowOffset = (x <= xMin || x >= xMax) ? (center - x) + 'px' : 'auto';
41+
Dom.setStyle(arrow, 'margin-left', arrowOffset);
42+
}
43+
44+
function notAnchor(node) { return 'A' !== node.nodeName; }
45+
function hasClass(className, node) { return Dom.hasClass(node, className); }
46+
47+
var LinksUi = {
48+
49+
/**
50+
* Opens the given context toolbar for editing the given anchor.
51+
*
52+
* @param {!Element} toolbar
53+
* @param {!Element} anchor
54+
*/
55+
open: function (toolbar, anchor) {
56+
var href = Dom.getAttr(anchor, 'href');
57+
$$('.aloha-active').removeClass('aloha-active');
58+
Dom.addClass(anchor, 'aloha-active');
59+
Dom.addClass(toolbar, 'opened');
60+
positionToolbar(toolbar, anchor);
61+
toolbar.querySelector('input').value = href;
62+
$$('a.aloha-link-follow').setAttr('href', href);
63+
},
64+
65+
/**
66+
* Closes the context toolbar.
67+
*
68+
* @param {!Element} toolbar
69+
*/
70+
close: function(toolbar) {
71+
$$('.aloha-active').removeClass('aloha-active');
72+
Dom.removeClass(toolbar, 'opened');
73+
},
74+
75+
/**
76+
* Retrieves a toolbar element from the given document if one exists.
77+
*
78+
* @param {!Document} doc
79+
* @return {?Element}
80+
*/
81+
toolbar: function (doc) {
82+
var toolbar = doc.querySelector('.aloha-link-toolbar');
83+
return (toolbar && Dom.hasClass(toolbar.parentNode, 'aloha-3d'))
84+
? toolbar.parentNode
85+
: toolbar;
86+
},
87+
88+
/**
89+
* Resolves the anchor element from the boundaries
90+
*
91+
* @param {Array.<Boundary>} boundaries
92+
* @return {?Element}
93+
*/
94+
anchor: function (boundaries) {
95+
var cac = Boundaries.commonContainer(boundaries[0], boundaries[1]);
96+
return Dom.upWhile(cac, notAnchor);
97+
},
98+
99+
/**
100+
* Returns the element or its first ancestor that has a 'aloha-ui'
101+
* class, if any.
102+
*
103+
* @param {!Element} element
104+
* @return {?Element}
105+
*/
106+
closestToolbar: function (element) {
107+
var toolbar = Dom.upWhile(element, Fn.complement(Fn.partial(hasClass, 'aloha-ui')));
108+
return (toolbar && Dom.hasClass(toolbar.parentNode, 'aloha-3d'))
109+
? toolbar.parentNode
110+
: toolbar;
111+
},
112+
113+
/**
114+
* Handles user interaction on the context toolbar.
115+
*
116+
* @param {!Element} element
117+
* @param {!Element} anchor
118+
*/
119+
interact: function (toolbar, anchor) {
120+
$$('a.aloha-active, a.aloha-link-follow').setAttr(
121+
'href',
122+
toolbar.querySelector('input').value
123+
);
124+
},
125+
126+
/**
127+
* Normalize boundaries, so that if either start
128+
* or end boundaries are inside an anchor tag
129+
* both boundaries will snap to that tag.
130+
* If the boundaries are collapsed, they will be
131+
* extended to word.
132+
*
133+
* @param {!Boundary} start
134+
* @param {!Boundary} end
135+
* @return {Array.<Boundary>}
136+
*/
137+
normalize: function (start, end) {
138+
var boundaries = [start, end];
139+
for (var i = 0; i < boundaries.length; i++) {
140+
var anchor = Dom.upWhile(Boundaries.container(boundaries[i]), notAnchor);
141+
if (anchor) {
142+
return [
143+
Boundaries.next(Boundaries.fromNode(anchor)),
144+
Boundaries.fromEndOfNode(anchor)
145+
];
146+
}
147+
}
148+
return Boundaries.equals(start, end)
149+
? Traversing.expand(start, end, 'word')
150+
: boundaries;
151+
},
152+
153+
/**
154+
* Inserts a link at the boundary position
155+
*
156+
* IMPORTANT: this function MUST be a named
157+
* function, because we will need to
158+
* prevent a selection update when a new link
159+
* is inserted later on.
160+
*
161+
* @param {!Boundary} start
162+
* @param {!Boundary} end
163+
* @return {Array.<Boundary>}
164+
*/
165+
insertLink: function insertLink (start, end) {
166+
var boundaries = LinksUi.normalize(start, end);
167+
if (Boundaries.container(boundaries[0]).nodeName !== 'A') {
168+
boundaries = Editing.wrap('A', boundaries[0], boundaries[1]);
169+
boundaries[0] = Boundaries.next(boundaries[0]);
170+
boundaries[1] = Boundaries.fromEndOfNode(boundaries[0])[0];
171+
}
172+
LinksUi.open(
173+
LinksUi.toolbar(document),
174+
Boundaries.container(boundaries[0])
175+
);
176+
$$('.aloha-link-toolbar input[name=href]').elements[0].focus();
177+
return boundaries;
178+
},
179+
180+
/**
181+
* Toggles the target attribute on any active anchor.
182+
*
183+
* @param {!Boundary} start
184+
* @param {!Boundary} end
185+
* @return {Array.<Boundary>}
186+
*/
187+
toggleTarget: function (start, end) {
188+
var anchor = $$('.aloha-active').elements[0];
189+
if (!anchor) {
190+
return [start, end];
191+
}
192+
if ('_blank' === Dom.getAttr(anchor, 'target')) {
193+
Dom.removeAttr(anchor, 'target');
194+
} else {
195+
Dom.setAttr(anchor, 'target', '_blank');
196+
}
197+
return [start, end];
198+
},
199+
200+
/**
201+
* Updates the ui according to any active anchor element.
202+
*/
203+
update: function (selection, formats) {
204+
if (!Arrays.contains(formats, 'A')) {
205+
return;
206+
}
207+
var anchor = $$('a.aloha-active').elements[0];
208+
if (!anchor) {
209+
return;
210+
}
211+
var href = Dom.getAttr(anchor, 'href');
212+
var target = Dom.getAttr(anchor, 'target');
213+
$$('.aloha-link-toolbar input[name=href]').value = href;
214+
if ('_blank' === target) {
215+
$$('.aloha-action-target').addClass('active');
216+
} else {
217+
$$('.aloha-action-target').removeClass('active');
218+
}
219+
}
220+
};
221+
222+
/**
223+
* Links-specific UI handling.
224+
*
225+
* @param {!Event} event
226+
* @return {Event}
227+
*/
228+
function handleLinksUi(event) {
229+
if ('keyup' !== event.type && 'click' !== event.type) {
230+
return event;
231+
}
232+
var anchor = LinksUi.anchor(event.selection.boundaries);
233+
var toolbar = LinksUi.toolbar(event.nativeEvent.target.ownerDocument);
234+
if (!toolbar) {
235+
return event;
236+
}
237+
if (anchor) {
238+
LinksUi.open(toolbar, anchor);
239+
return event;
240+
}
241+
if (toolbar === LinksUi.closestToolbar(event.nativeEvent.target)) {
242+
LinksUi.interact(toolbar, anchor, event);
243+
return event;
244+
}
245+
LinksUi.close(toolbar);
246+
return event;
247+
}
248+
249+
/*
250+
* link toolbar interactions
251+
*/
252+
$$('.aloha-link-toolbar input[name=href]').on('keyup', function (event) {
253+
if (Editor.selection) {
254+
LinksUi.interact(
255+
LinksUi.toolbar(event.target.ownerDocument),
256+
LinksUi.anchor(Editor.selection.boundaries)
257+
);
258+
}
259+
260+
var shortcuts = {
261+
'enter' : function () {
262+
var anchor = $$('a.aloha-active').elements[0];
263+
var href = $$('.aloha-link-toolbar input[name=href]').elements[0];
264+
var boundary = Boundaries.next(Boundaries.fromEndOfNode(anchor));
265+
Editor.selection = Selections.select(
266+
Editor.selection,
267+
boundary,
268+
boundary
269+
);
270+
aloha.editor.ui.update(Editor.selection);
271+
if (!href.value) {
272+
Dom.removeShallow(anchor);
273+
}
274+
return [boundary, boundary];
275+
}
276+
};
277+
var key = Keys.parseKeys(event);
278+
var handler = Keys.shortcutHandler(key.meta, key.keycode, shortcuts);
279+
if (handler) {
280+
Editor.selection.boundaries = handler(
281+
Editor.selection.boundaries[0],
282+
Editor.selection.boundaries[1]
283+
);
284+
}
285+
});
286+
287+
aloha.editor.ui.actions = Maps.merge(aloha.editor.ui.actions, {
288+
'aloha-action-A' : LinksUi.insertLink,
289+
'aloha-action-target' : LinksUi.toggleTarget
290+
});
291+
292+
aloha.editor.ui.shortcuts = Maps.merge(aloha.editor.ui.shortcuts, {
293+
'meta+k' : LinksUi.insertLink,
294+
'ctrl+k' : LinksUi.insertLink
295+
});
296+
297+
aloha.editor.ui.updateHandlers.push(LinksUi.update);
298+
299+
// put handleLinksUi on the editor stack BEFORE handleUi
300+
aloha.editor.stack = aloha.editor.stack.reduce(function (previousValue, currentValue) {
301+
var arr = previousValue.concat(currentValue);
302+
return (currentValue.name === 'handleUi') ? arr.concat(handleLinksUi) : arr;
303+
}, []);
304+
})(window.aloha);

0 commit comments

Comments
 (0)