4 var inputs = {}, rivers = {}, ed, River, Query;
7 timeToTriggerRiver: 150,
8 minRiverAJAXDuration: 200,
9 riverBottomThreshold: 5,
15 inputs.dialog = $('#wp-link');
16 inputs.submit = $('#wp-link-submit');
18 inputs.url = $('#url-field');
19 inputs.nonce = $('#_ajax_linking_nonce');
21 inputs.title = $('#link-title-field');
23 inputs.openInNewTab = $('#link-target-checkbox');
24 inputs.search = $('#search-field');
26 rivers.search = new River( $('#search-results') );
27 rivers.recent = new River( $('#most-recent-results') );
28 rivers.elements = $('.query-results', inputs.dialog);
30 // Bind event handlers
31 inputs.dialog.keydown( wpLink.keydown );
32 inputs.dialog.keyup( wpLink.keyup );
33 inputs.submit.click( function(e){
37 $('#wp-link-cancel').click( function(e){
41 $('#internal-toggle').click( wpLink.toggleInternalLinking );
43 rivers.elements.bind('river-select', wpLink.updateFields );
45 inputs.search.keyup( wpLink.searchInternalLinks );
47 inputs.dialog.bind('wpdialogrefresh', wpLink.refresh);
48 inputs.dialog.bind('wpdialogbeforeopen', wpLink.beforeOpen);
49 inputs.dialog.bind('wpdialogclose', wpLink.onClose);
52 beforeOpen : function() {
55 if ( ! wpLink.isMCE() && document.selection ) {
56 wpLink.textarea.focus();
57 wpLink.range = document.selection.createRange();
62 if ( !wpActiveEditor )
65 this.textarea = $('#'+wpActiveEditor).get(0);
67 // Initialize the dialog if necessary (html mode).
68 if ( ! inputs.dialog.data('wpdialog') ) {
69 inputs.dialog.wpdialog({
70 title: wpLinkL10n.title,
74 dialogClass: 'wp-dialog',
79 inputs.dialog.wpdialog('open');
83 return tinyMCEPopup && ( ed = tinyMCEPopup.editor ) && ! ed.isHidden();
86 refresh : function() {
87 // Refresh rivers (clear links, check visibility)
88 rivers.search.refresh();
89 rivers.recent.refresh();
94 wpLink.setDefaultValues();
96 // Focus the URL field and highlight its contents.
97 // If this is moved above the selection changes,
98 // IE will show a flashing cursor over the dialog.
99 inputs.url.focus()[0].select();
100 // Load the most recent results if this is the first time opening the panel.
101 if ( ! rivers.recent.ul.children().length )
102 rivers.recent.ajax();
105 mceRefresh : function() {
107 ed = tinyMCEPopup.editor;
109 tinyMCEPopup.restoreSelection();
111 // If link exists, select proper values.
112 if ( e = ed.dom.getParent(ed.selection.getNode(), 'A') ) {
113 // Set URL and description.
114 inputs.url.val( ed.dom.getAttrib(e, 'href') );
115 inputs.title.val( ed.dom.getAttrib(e, 'title') );
116 // Set open in new tab.
117 if ( "_blank" == ed.dom.getAttrib(e, 'target') )
118 inputs.openInNewTab.prop('checked', true);
119 // Update save prompt.
120 inputs.submit.val( wpLinkL10n.update );
122 // If there's no link, set the default values.
124 wpLink.setDefaultValues();
127 tinyMCEPopup.storeSelection();
131 if ( wpLink.isMCE() )
132 tinyMCEPopup.close();
134 inputs.dialog.wpdialog('close');
137 onClose: function() {
138 if ( ! wpLink.isMCE() ) {
139 wpLink.textarea.focus();
140 if ( wpLink.range ) {
141 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
142 wpLink.range.select();
147 getAttrs : function() {
149 href : inputs.url.val(),
150 title : inputs.title.val(),
151 target : inputs.openInNewTab.prop('checked') ? '_blank' : ''
155 update : function() {
156 if ( wpLink.isMCE() )
162 htmlUpdate : function() {
163 var attrs, html, begin, end, cursor,
164 textarea = wpLink.textarea;
169 attrs = wpLink.getAttrs();
171 // If there's no href, return.
172 if ( ! attrs.href || attrs.href == 'http://' )
176 html = '<a href="' + attrs.href + '"';
179 html += ' title="' + attrs.title + '"';
181 html += ' target="' + attrs.target + '"';
186 if ( document.selection && wpLink.range ) {
188 // Note: If no text is selected, IE will not place the cursor
189 // inside the closing tag.
191 wpLink.range.text = html + wpLink.range.text + '</a>';
192 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
193 wpLink.range.select();
196 } else if ( typeof textarea.selectionStart !== 'undefined' ) {
198 begin = textarea.selectionStart;
199 end = textarea.selectionEnd;
200 selection = textarea.value.substring( begin, end );
201 html = html + selection + '</a>';
202 cursor = begin + html.length;
204 // If no next is selected, place the cursor inside the closing tag.
206 cursor -= '</a>'.length;
208 textarea.value = textarea.value.substring( 0, begin )
210 + textarea.value.substring( end, textarea.value.length );
212 // Update cursor position
213 textarea.selectionStart = textarea.selectionEnd = cursor;
220 mceUpdate : function() {
221 var ed = tinyMCEPopup.editor,
222 attrs = wpLink.getAttrs(),
225 tinyMCEPopup.restoreSelection();
226 e = ed.dom.getParent(ed.selection.getNode(), 'A');
228 // If the values are empty, unlink and return
229 if ( ! attrs.href || attrs.href == 'http://' ) {
231 tinyMCEPopup.execCommand("mceBeginUndoLevel");
232 b = ed.selection.getBookmark();
234 ed.selection.moveToBookmark(b);
235 tinyMCEPopup.execCommand("mceEndUndoLevel");
241 tinyMCEPopup.execCommand("mceBeginUndoLevel");
244 ed.getDoc().execCommand("unlink", false, null);
245 tinyMCEPopup.execCommand("mceInsertLink", false, "#mce_temp_url#", {skip_undo : 1});
247 tinymce.each(ed.dom.select("a"), function(n) {
248 if (ed.dom.getAttrib(n, 'href') == '#mce_temp_url#') {
250 ed.dom.setAttribs(e, attrs);
254 // Sometimes WebKit lets a user create a link where
255 // they shouldn't be able to. In this case, CreateLink
256 // injects "#mce_temp_url#" into their content. Fix it.
257 if ( $(e).text() == '#mce_temp_url#' ) {
262 ed.dom.setAttribs(e, attrs);
265 // Don't move caret if selection was image
266 if ( e && (e.childNodes.length != 1 || e.firstChild.nodeName != 'IMG') ) {
268 ed.selection.select(e);
269 ed.selection.collapse(0);
270 tinyMCEPopup.storeSelection();
273 tinyMCEPopup.execCommand("mceEndUndoLevel");
277 updateFields : function( e, li, originalEvent ) {
278 inputs.url.val( li.children('.item-permalink').val() );
279 inputs.title.val( li.hasClass('no-title') ? '' : li.children('.item-title').text() );
280 if ( originalEvent && originalEvent.type == "click" )
283 setDefaultValues : function() {
284 // Set URL and description to defaults.
285 // Leave the new tab setting as-is.
286 inputs.url.val('http://');
287 inputs.title.val('');
289 // Update save prompt.
290 inputs.submit.val( wpLinkL10n.save );
293 searchInternalLinks : function() {
294 var t = $(this), waiting,
297 if ( search.length > 2 ) {
298 rivers.recent.hide();
299 rivers.search.show();
301 // Don't search if the keypress didn't change the title.
302 if ( wpLink.lastSearch == search )
305 wpLink.lastSearch = search;
306 waiting = t.siblings('img.waiting').show();
308 rivers.search.change( search );
309 rivers.search.ajax( function(){ waiting.hide(); });
311 rivers.search.hide();
312 rivers.recent.show();
317 rivers.search.next();
318 rivers.recent.next();
321 rivers.search.prev();
322 rivers.recent.prev();
325 keydown : function( event ) {
326 var fn, key = $.ui.keyCode;
328 switch( event.which ) {
333 clearInterval( wpLink.keyInterval );
335 wpLink.keyInterval = setInterval( wpLink[ fn ], wpLink.keySensitivity );
340 event.preventDefault();
342 keyup: function( event ) {
343 var key = $.ui.keyCode;
345 switch( event.which ) {
347 event.stopImmediatePropagation();
348 if ( ! $(document).triggerHandler( 'wp_CloseOnEscape', [{ event: event, what: 'wplink', cb: wpLink.close }] ) )
355 clearInterval( wpLink.keyInterval );
360 event.preventDefault();
363 delayedCallback : function( func, delay ) {
364 var timeoutTriggered, funcTriggered, funcArgs, funcContext;
369 setTimeout( function() {
371 return func.apply( funcContext, funcArgs );
373 timeoutTriggered = true;
377 if ( timeoutTriggered )
378 return func.apply( this, arguments );
380 funcArgs = arguments;
382 funcTriggered = true;
386 toggleInternalLinking : function( event ) {
387 var panel = $('#search-panel'),
388 widget = inputs.dialog.wpdialog('widget'),
389 // We're about to toggle visibility; it's currently the opposite
390 visible = !panel.is(':visible'),
393 $(this).toggleClass('toggle-arrow-active', visible);
395 inputs.dialog.height('auto');
396 panel.slideToggle( 300, function() {
397 setUserSetting('wplink', visible ? '1' : '0');
398 inputs[ visible ? 'search' : 'url' ].focus();
400 // Move the box if the box is now expanded, was opened in a collapsed state,
401 // and if it needs to be moved. (Judged by bottom not being positive or
402 // bottom being smaller than top.)
403 var scroll = win.scrollTop(),
404 top = widget.offset().top,
405 bottom = top + widget.outerHeight(),
406 diff = bottom - win.height();
408 if ( diff > scroll ) {
409 widget.animate({'top': diff < top ? top - diff : scroll }, 200);
412 event.preventDefault();
416 River = function( element, search ) {
418 this.element = element;
419 this.ul = element.children('ul');
420 this.waiting = element.find('.river-waiting');
422 this.change( search );
425 element.scroll( function(){ self.maybeLoad(); });
426 element.delegate('li', 'click', function(e){ self.select( $(this), e ); });
429 $.extend( River.prototype, {
430 refresh: function() {
432 this.visible = this.element.is(':visible');
435 if ( ! this.visible ) {
443 this.visible = false;
445 // Selects a list item and triggers the river-select event.
446 select: function( li, event ) {
447 var liHeight, elHeight, liTop, elTop;
449 if ( li.hasClass('unselectable') || li == this.selected )
453 this.selected = li.addClass('selected');
454 // Make sure the element is visible
455 liHeight = li.outerHeight();
456 elHeight = this.element.height();
457 liTop = li.position().top;
458 elTop = this.element.scrollTop();
460 if ( liTop < 0 ) // Make first visible element
461 this.element.scrollTop( elTop + liTop );
462 else if ( liTop + liHeight > elHeight ) // Make last visible element
463 this.element.scrollTop( elTop + liTop - elHeight + liHeight );
465 // Trigger the river-select event
466 this.element.trigger('river-select', [ li, event, this ]);
468 deselect: function() {
470 this.selected.removeClass('selected');
471 this.selected = false;
474 if ( ! this.visible )
478 if ( this.selected ) {
479 to = this.selected.prev('li');
485 if ( ! this.visible )
488 var to = this.selected ? this.selected.next('li') : $('li:not(.unselectable):first', this.element);
492 ajax: function( callback ) {
494 delay = this.query.page == 1 ? 0 : wpLink.minRiverAJAXDuration,
495 response = wpLink.delayedCallback( function( results, params ) {
496 self.process( results, params );
498 callback( results, params );
501 this.query.ajax( response );
503 change: function( search ) {
504 if ( this.query && this._search == search )
507 this._search = search;
508 this.query = new Query( search );
509 this.element.scrollTop(0);
511 process: function( results, params ) {
512 var list = '', alt = true, classes = '',
513 firstPage = params.page == 1;
517 list += '<li class="unselectable"><span class="item-title"><em>'
518 + wpLinkL10n.noMatchesFound
519 + '</em></span></li>';
522 $.each( results, function() {
523 classes = alt ? 'alternate' : '';
524 classes += this['title'] ? '' : ' no-title';
525 list += classes ? '<li class="' + classes + '">' : '<li>';
526 list += '<input type="hidden" class="item-permalink" value="' + this['permalink'] + '" />';
527 list += '<span class="item-title">';
528 list += this['title'] ? this['title'] : wpLinkL10n.noTitle;
529 list += '</span><span class="item-info">' + this['info'] + '</span></li>';
534 this.ul[ firstPage ? 'html' : 'append' ]( list );
536 maybeLoad: function() {
539 bottom = el.scrollTop() + el.height();
541 if ( ! this.query.ready() || bottom < this.ul.height() - wpLink.riverBottomThreshold )
544 setTimeout(function() {
545 var newTop = el.scrollTop(),
546 newBottom = newTop + el.height();
548 if ( ! self.query.ready() || newBottom < self.ul.height() - wpLink.riverBottomThreshold )
552 el.scrollTop( newTop + self.waiting.outerHeight() );
554 self.ajax( function() { self.waiting.hide(); });
555 }, wpLink.timeToTriggerRiver );
559 Query = function( search ) {
561 this.allLoaded = false;
562 this.querying = false;
563 this.search = search;
566 $.extend( Query.prototype, {
568 return !( this.querying || this.allLoaded );
570 ajax: function( callback ) {
573 action : 'wp-link-ajax',
575 '_ajax_linking_nonce' : inputs.nonce.val()
579 query.search = this.search;
581 this.querying = true;
583 $.post( ajaxurl, query, function(r) {
585 self.querying = false;
587 callback( r, query );
592 $(document).ready( wpLink.init );