4 var inputs = {}, rivers = {}, ed, River, Query;
7 timeToTriggerRiver: 150,
8 minRiverAJAXDuration: 200,
9 riverBottomThreshold: 5,
12 textarea: function() { return edCanvas; },
15 inputs.dialog = $('#wp-link');
16 inputs.submit = $('#wp-link-submit');
18 inputs.url = $('#url-field');
20 inputs.title = $('#link-title-field');
22 inputs.openInNewTab = $('#link-target-checkbox');
23 inputs.search = $('#search-field');
25 rivers.search = new River( $('#search-results') );
26 rivers.recent = new River( $('#most-recent-results') );
27 rivers.elements = $('.query-results', inputs.dialog);
29 // Bind event handlers
30 inputs.dialog.keydown( wpLink.keydown );
31 inputs.dialog.keyup( wpLink.keyup );
32 inputs.submit.click( function(e){
36 $('#wp-link-cancel').click( wpLink.close );
37 $('#internal-toggle').click( wpLink.toggleInternalLinking );
39 rivers.elements.bind('river-select', wpLink.updateFields );
41 inputs.search.keyup( wpLink.searchInternalLinks );
43 inputs.dialog.bind('wpdialogrefresh', wpLink.refresh);
44 inputs.dialog.bind('wpdialogbeforeopen', wpLink.beforeOpen);
45 inputs.dialog.bind('wpdialogclose', wpLink.onClose);
48 beforeOpen : function() {
51 if ( ! wpLink.isMCE() && document.selection ) {
52 wpLink.textarea().focus();
53 wpLink.range = document.selection.createRange();
58 // Initialize the dialog if necessary (html mode).
59 if ( ! inputs.dialog.data('wpdialog') ) {
60 inputs.dialog.wpdialog({
61 title: wpLinkL10n.title,
65 dialogClass: 'wp-dialog',
70 inputs.dialog.wpdialog('open');
74 return tinyMCEPopup && ( ed = tinyMCEPopup.editor ) && ! ed.isHidden();
77 refresh : function() {
78 // Refresh rivers (clear links, check visibility)
79 rivers.search.refresh();
80 rivers.recent.refresh();
85 wpLink.setDefaultValues();
87 // Focus the URL field and highlight its contents.
88 // If this is moved above the selection changes,
89 // IE will show a flashing cursor over the dialog.
90 inputs.url.focus()[0].select();
91 // Load the most recent results if this is the first time opening the panel.
92 if ( ! rivers.recent.ul.children().length )
96 mceRefresh : function() {
98 ed = tinyMCEPopup.editor;
100 tinyMCEPopup.restoreSelection();
102 // If link exists, select proper values.
103 if ( e = ed.dom.getParent(ed.selection.getNode(), 'A') ) {
104 // Set URL and description.
105 inputs.url.val( e.href );
106 inputs.title.val( ed.dom.getAttrib(e, 'title') );
107 // Set open in new tab.
108 if ( "_blank" == ed.dom.getAttrib(e, 'target') )
109 inputs.openInNewTab.prop('checked', true);
110 // Update save prompt.
111 inputs.submit.val( wpLinkL10n.update );
113 // If there's no link, set the default values.
115 wpLink.setDefaultValues();
118 tinyMCEPopup.storeSelection();
122 if ( wpLink.isMCE() )
123 tinyMCEPopup.close();
125 inputs.dialog.wpdialog('close');
128 onClose: function() {
129 if ( ! wpLink.isMCE() ) {
130 wpLink.textarea().focus();
131 if ( wpLink.range ) {
132 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
133 wpLink.range.select();
138 getAttrs : function() {
140 href : inputs.url.val(),
141 title : inputs.title.val(),
142 target : inputs.openInNewTab.prop('checked') ? '_blank' : ''
146 update : function() {
147 if ( wpLink.isMCE() )
153 htmlUpdate : function() {
154 var attrs, html, start, end, cursor,
155 textarea = wpLink.textarea();
160 attrs = wpLink.getAttrs();
162 // If there's no href, return.
163 if ( ! attrs.href || attrs.href == 'http://' )
167 html = '<a href="' + attrs.href + '"';
170 html += ' title="' + attrs.title + '"';
172 html += ' target="' + attrs.target + '"';
178 if ( typeof textarea.selectionStart !== 'undefined' ) {
179 start = textarea.selectionStart;
180 end = textarea.selectionEnd;
181 selection = textarea.value.substring( start, end );
182 html = html + selection + '</a>';
183 cursor = start + html.length;
185 // If no next is selected, place the cursor inside the closing tag.
187 cursor -= '</a>'.length;
189 textarea.value = textarea.value.substring( 0, start )
191 + textarea.value.substring( end, textarea.value.length );
193 // Update cursor position
194 textarea.selectionStart = textarea.selectionEnd = cursor;
197 // Note: If no text is selected, IE will not place the cursor
198 // inside the closing tag.
199 } else if ( document.selection && wpLink.range ) {
201 wpLink.range.text = html + wpLink.range.text + '</a>';
202 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
203 wpLink.range.select();
212 mceUpdate : function() {
213 var ed = tinyMCEPopup.editor,
214 attrs = wpLink.getAttrs(),
217 tinyMCEPopup.restoreSelection();
218 e = ed.dom.getParent(ed.selection.getNode(), 'A');
220 // If the values are empty, unlink and return
221 if ( ! attrs.href || attrs.href == 'http://' ) {
223 tinyMCEPopup.execCommand("mceBeginUndoLevel");
224 b = ed.selection.getBookmark();
226 ed.selection.moveToBookmark(b);
227 tinyMCEPopup.execCommand("mceEndUndoLevel");
233 tinyMCEPopup.execCommand("mceBeginUndoLevel");
236 ed.getDoc().execCommand("unlink", false, null);
237 tinyMCEPopup.execCommand("CreateLink", false, "#mce_temp_url#", {skip_undo : 1});
239 tinymce.each(ed.dom.select("a"), function(n) {
240 if (ed.dom.getAttrib(n, 'href') == '#mce_temp_url#') {
242 ed.dom.setAttribs(e, attrs);
246 // Sometimes WebKit lets a user create a link where
247 // they shouldn't be able to. In this case, CreateLink
248 // injects "#mce_temp_url#" into their content. Fix it.
249 if ( $(e).text() == '#mce_temp_url#' ) {
254 ed.dom.setAttribs(e, attrs);
257 // Don't move caret if selection was image
258 if ( e && (e.childNodes.length != 1 || e.firstChild.nodeName != 'IMG') ) {
260 ed.selection.select(e);
261 ed.selection.collapse(0);
262 tinyMCEPopup.storeSelection();
265 tinyMCEPopup.execCommand("mceEndUndoLevel");
269 updateFields : function( e, li, originalEvent ) {
270 inputs.url.val( li.children('.item-permalink').val() );
271 inputs.title.val( li.hasClass('no-title') ? '' : li.children('.item-title').text() );
272 if ( originalEvent && originalEvent.type == "click" )
275 setDefaultValues : function() {
276 // Set URL and description to defaults.
277 // Leave the new tab setting as-is.
278 inputs.url.val('http://');
279 inputs.title.val('');
281 // Update save prompt.
282 inputs.submit.val( wpLinkL10n.save );
285 searchInternalLinks : function() {
286 var t = $(this), waiting,
289 if ( search.length > 2 ) {
290 rivers.recent.hide();
291 rivers.search.show();
293 // Don't search if the keypress didn't change the title.
294 if ( wpLink.lastSearch == search )
297 wpLink.lastSearch = search;
298 waiting = t.siblings('img.waiting').show();
300 rivers.search.change( search );
301 rivers.search.ajax( function(){ waiting.hide(); });
303 rivers.search.hide();
304 rivers.recent.show();
309 rivers.search.next();
310 rivers.recent.next();
313 rivers.search.prev();
314 rivers.recent.prev();
317 keydown : function( event ) {
318 var fn, key = $.ui.keyCode;
320 switch( event.which ) {
325 clearInterval( wpLink.keyInterval );
327 wpLink.keyInterval = setInterval( wpLink[ fn ], wpLink.keySensitivity );
332 event.preventDefault();
334 keyup: function( event ) {
335 var key = $.ui.keyCode;
337 switch( event.which ) {
339 event.stopImmediatePropagation();
340 if ( ! $(document).triggerHandler( 'wp_CloseOnEscape', [{ event: event, what: 'wplink', cb: wpLink.close }] ) )
347 clearInterval( wpLink.keyInterval );
352 event.preventDefault();
355 delayedCallback : function( func, delay ) {
356 var timeoutTriggered, funcTriggered, funcArgs, funcContext;
361 setTimeout( function() {
363 return func.apply( funcContext, funcArgs );
365 timeoutTriggered = true;
369 if ( timeoutTriggered )
370 return func.apply( this, arguments );
372 funcArgs = arguments;
374 funcTriggered = true;
378 toggleInternalLinking : function( event ) {
379 var panel = $('#search-panel'),
380 widget = inputs.dialog.wpdialog('widget'),
381 // We're about to toggle visibility; it's currently the opposite
382 visible = !panel.is(':visible'),
385 $(this).toggleClass('toggle-arrow-active', visible);
387 inputs.dialog.height('auto');
388 panel.slideToggle( 300, function() {
389 setUserSetting('wplink', visible ? '1' : '0');
390 inputs[ visible ? 'search' : 'url' ].focus();
392 // Move the box if the box is now expanded, was opened in a collapsed state,
393 // and if it needs to be moved. (Judged by bottom not being positive or
394 // bottom being smaller than top.)
395 var scroll = win.scrollTop(),
396 top = widget.offset().top,
397 bottom = top + widget.outerHeight(),
398 diff = bottom - win.height();
400 if ( diff > scroll ) {
401 widget.animate({'top': diff < top ? top - diff : scroll }, 200);
404 event.preventDefault();
408 River = function( element, search ) {
410 this.element = element;
411 this.ul = element.children('ul');
412 this.waiting = element.find('.river-waiting');
414 this.change( search );
417 element.scroll( function(){ self.maybeLoad(); });
418 element.delegate('li', 'click', function(e){ self.select( $(this), e ); });
421 $.extend( River.prototype, {
422 refresh: function() {
424 this.visible = this.element.is(':visible');
427 if ( ! this.visible ) {
435 this.visible = false;
437 // Selects a list item and triggers the river-select event.
438 select: function( li, event ) {
439 var liHeight, elHeight, liTop, elTop;
441 if ( li.hasClass('unselectable') || li == this.selected )
445 this.selected = li.addClass('selected');
446 // Make sure the element is visible
447 liHeight = li.outerHeight();
448 elHeight = this.element.height();
449 liTop = li.position().top;
450 elTop = this.element.scrollTop();
452 if ( liTop < 0 ) // Make first visible element
453 this.element.scrollTop( elTop + liTop );
454 else if ( liTop + liHeight > elHeight ) // Make last visible element
455 this.element.scrollTop( elTop + liTop - elHeight + liHeight );
457 // Trigger the river-select event
458 this.element.trigger('river-select', [ li, event, this ]);
460 deselect: function() {
462 this.selected.removeClass('selected');
463 this.selected = false;
466 if ( ! this.visible )
470 if ( this.selected ) {
471 to = this.selected.prev('li');
477 if ( ! this.visible )
480 var to = this.selected ? this.selected.next('li') : $('li:not(.unselectable):first', this.element);
484 ajax: function( callback ) {
486 delay = this.query.page == 1 ? 0 : wpLink.minRiverAJAXDuration,
487 response = wpLink.delayedCallback( function( results, params ) {
488 self.process( results, params );
490 callback( results, params );
493 this.query.ajax( response );
495 change: function( search ) {
496 if ( this.query && this._search == search )
499 this._search = search;
500 this.query = new Query( search );
501 this.element.scrollTop(0);
503 process: function( results, params ) {
504 var list = '', alt = true, classes = '',
505 firstPage = params.page == 1;
509 list += '<li class="unselectable"><span class="item-title"><em>'
510 + wpLinkL10n.noMatchesFound
511 + '</em></span></li>';
514 $.each( results, function() {
515 classes = alt ? 'alternate' : '';
516 classes += this['title'] ? '' : ' no-title';
517 list += classes ? '<li class="' + classes + '">' : '<li>';
518 list += '<input type="hidden" class="item-permalink" value="' + this['permalink'] + '" />';
519 list += '<span class="item-title">';
520 list += this['title'] ? this['title'] : wpLinkL10n.noTitle;
521 list += '</span><span class="item-info">' + this['info'] + '</span></li>';
526 this.ul[ firstPage ? 'html' : 'append' ]( list );
528 maybeLoad: function() {
531 bottom = el.scrollTop() + el.height();
533 if ( ! this.query.ready() || bottom < this.ul.height() - wpLink.riverBottomThreshold )
536 setTimeout(function() {
537 var newTop = el.scrollTop(),
538 newBottom = newTop + el.height();
540 if ( ! self.query.ready() || newBottom < self.ul.height() - wpLink.riverBottomThreshold )
544 el.scrollTop( newTop + self.waiting.outerHeight() );
546 self.ajax( function() { self.waiting.hide(); });
547 }, wpLink.timeToTriggerRiver );
551 Query = function( search ) {
553 this.allLoaded = false;
554 this.querying = false;
555 this.search = search;
558 $.extend( Query.prototype, {
560 return !( this.querying || this.allLoaded );
562 ajax: function( callback ) {
565 action : 'wp-link-ajax',
567 '_ajax_linking_nonce' : $('#_ajax_linking_nonce').val()
571 query.search = this.search;
573 this.querying = true;
575 $.post( ajaxurl, query, function(r) {
577 self.querying = false;
579 callback( r, query );
584 $(document).ready( wpLink.init );