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, start, 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 + '"';
187 if ( typeof textarea.selectionStart !== 'undefined' ) {
188 start = textarea.selectionStart;
189 end = textarea.selectionEnd;
190 selection = textarea.value.substring( start, end );
191 html = html + selection + '</a>';
192 cursor = start + html.length;
194 // If no next is selected, place the cursor inside the closing tag.
196 cursor -= '</a>'.length;
198 textarea.value = textarea.value.substring( 0, start )
200 + textarea.value.substring( end, textarea.value.length );
202 // Update cursor position
203 textarea.selectionStart = textarea.selectionEnd = cursor;
206 // Note: If no text is selected, IE will not place the cursor
207 // inside the closing tag.
208 } else if ( document.selection && wpLink.range ) {
210 wpLink.range.text = html + wpLink.range.text + '</a>';
211 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
212 wpLink.range.select();
221 mceUpdate : function() {
222 var ed = tinyMCEPopup.editor,
223 attrs = wpLink.getAttrs(),
226 tinyMCEPopup.restoreSelection();
227 e = ed.dom.getParent(ed.selection.getNode(), 'A');
229 // If the values are empty, unlink and return
230 if ( ! attrs.href || attrs.href == 'http://' ) {
232 tinyMCEPopup.execCommand("mceBeginUndoLevel");
233 b = ed.selection.getBookmark();
235 ed.selection.moveToBookmark(b);
236 tinyMCEPopup.execCommand("mceEndUndoLevel");
242 tinyMCEPopup.execCommand("mceBeginUndoLevel");
245 ed.getDoc().execCommand("unlink", false, null);
246 tinyMCEPopup.execCommand("CreateLink", false, "#mce_temp_url#", {skip_undo : 1});
248 tinymce.each(ed.dom.select("a"), function(n) {
249 if (ed.dom.getAttrib(n, 'href') == '#mce_temp_url#') {
251 ed.dom.setAttribs(e, attrs);
255 // Sometimes WebKit lets a user create a link where
256 // they shouldn't be able to. In this case, CreateLink
257 // injects "#mce_temp_url#" into their content. Fix it.
258 if ( $(e).text() == '#mce_temp_url#' ) {
263 ed.dom.setAttribs(e, attrs);
266 // Don't move caret if selection was image
267 if ( e && (e.childNodes.length != 1 || e.firstChild.nodeName != 'IMG') ) {
269 ed.selection.select(e);
270 ed.selection.collapse(0);
271 tinyMCEPopup.storeSelection();
274 tinyMCEPopup.execCommand("mceEndUndoLevel");
278 updateFields : function( e, li, originalEvent ) {
279 inputs.url.val( li.children('.item-permalink').val() );
280 inputs.title.val( li.hasClass('no-title') ? '' : li.children('.item-title').text() );
281 if ( originalEvent && originalEvent.type == "click" )
284 setDefaultValues : function() {
285 // Set URL and description to defaults.
286 // Leave the new tab setting as-is.
287 inputs.url.val('http://');
288 inputs.title.val('');
290 // Update save prompt.
291 inputs.submit.val( wpLinkL10n.save );
294 searchInternalLinks : function() {
295 var t = $(this), waiting,
298 if ( search.length > 2 ) {
299 rivers.recent.hide();
300 rivers.search.show();
302 // Don't search if the keypress didn't change the title.
303 if ( wpLink.lastSearch == search )
306 wpLink.lastSearch = search;
307 waiting = t.siblings('img.waiting').show();
309 rivers.search.change( search );
310 rivers.search.ajax( function(){ waiting.hide(); });
312 rivers.search.hide();
313 rivers.recent.show();
318 rivers.search.next();
319 rivers.recent.next();
322 rivers.search.prev();
323 rivers.recent.prev();
326 keydown : function( event ) {
327 var fn, key = $.ui.keyCode;
329 switch( event.which ) {
334 clearInterval( wpLink.keyInterval );
336 wpLink.keyInterval = setInterval( wpLink[ fn ], wpLink.keySensitivity );
341 event.preventDefault();
343 keyup: function( event ) {
344 var key = $.ui.keyCode;
346 switch( event.which ) {
348 event.stopImmediatePropagation();
349 if ( ! $(document).triggerHandler( 'wp_CloseOnEscape', [{ event: event, what: 'wplink', cb: wpLink.close }] ) )
356 clearInterval( wpLink.keyInterval );
361 event.preventDefault();
364 delayedCallback : function( func, delay ) {
365 var timeoutTriggered, funcTriggered, funcArgs, funcContext;
370 setTimeout( function() {
372 return func.apply( funcContext, funcArgs );
374 timeoutTriggered = true;
378 if ( timeoutTriggered )
379 return func.apply( this, arguments );
381 funcArgs = arguments;
383 funcTriggered = true;
387 toggleInternalLinking : function( event ) {
388 var panel = $('#search-panel'),
389 widget = inputs.dialog.wpdialog('widget'),
390 // We're about to toggle visibility; it's currently the opposite
391 visible = !panel.is(':visible'),
394 $(this).toggleClass('toggle-arrow-active', visible);
396 inputs.dialog.height('auto');
397 panel.slideToggle( 300, function() {
398 setUserSetting('wplink', visible ? '1' : '0');
399 inputs[ visible ? 'search' : 'url' ].focus();
401 // Move the box if the box is now expanded, was opened in a collapsed state,
402 // and if it needs to be moved. (Judged by bottom not being positive or
403 // bottom being smaller than top.)
404 var scroll = win.scrollTop(),
405 top = widget.offset().top,
406 bottom = top + widget.outerHeight(),
407 diff = bottom - win.height();
409 if ( diff > scroll ) {
410 widget.animate({'top': diff < top ? top - diff : scroll }, 200);
413 event.preventDefault();
417 River = function( element, search ) {
419 this.element = element;
420 this.ul = element.children('ul');
421 this.waiting = element.find('.river-waiting');
423 this.change( search );
426 element.scroll( function(){ self.maybeLoad(); });
427 element.delegate('li', 'click', function(e){ self.select( $(this), e ); });
430 $.extend( River.prototype, {
431 refresh: function() {
433 this.visible = this.element.is(':visible');
436 if ( ! this.visible ) {
444 this.visible = false;
446 // Selects a list item and triggers the river-select event.
447 select: function( li, event ) {
448 var liHeight, elHeight, liTop, elTop;
450 if ( li.hasClass('unselectable') || li == this.selected )
454 this.selected = li.addClass('selected');
455 // Make sure the element is visible
456 liHeight = li.outerHeight();
457 elHeight = this.element.height();
458 liTop = li.position().top;
459 elTop = this.element.scrollTop();
461 if ( liTop < 0 ) // Make first visible element
462 this.element.scrollTop( elTop + liTop );
463 else if ( liTop + liHeight > elHeight ) // Make last visible element
464 this.element.scrollTop( elTop + liTop - elHeight + liHeight );
466 // Trigger the river-select event
467 this.element.trigger('river-select', [ li, event, this ]);
469 deselect: function() {
471 this.selected.removeClass('selected');
472 this.selected = false;
475 if ( ! this.visible )
479 if ( this.selected ) {
480 to = this.selected.prev('li');
486 if ( ! this.visible )
489 var to = this.selected ? this.selected.next('li') : $('li:not(.unselectable):first', this.element);
493 ajax: function( callback ) {
495 delay = this.query.page == 1 ? 0 : wpLink.minRiverAJAXDuration,
496 response = wpLink.delayedCallback( function( results, params ) {
497 self.process( results, params );
499 callback( results, params );
502 this.query.ajax( response );
504 change: function( search ) {
505 if ( this.query && this._search == search )
508 this._search = search;
509 this.query = new Query( search );
510 this.element.scrollTop(0);
512 process: function( results, params ) {
513 var list = '', alt = true, classes = '',
514 firstPage = params.page == 1;
518 list += '<li class="unselectable"><span class="item-title"><em>'
519 + wpLinkL10n.noMatchesFound
520 + '</em></span></li>';
523 $.each( results, function() {
524 classes = alt ? 'alternate' : '';
525 classes += this['title'] ? '' : ' no-title';
526 list += classes ? '<li class="' + classes + '">' : '<li>';
527 list += '<input type="hidden" class="item-permalink" value="' + this['permalink'] + '" />';
528 list += '<span class="item-title">';
529 list += this['title'] ? this['title'] : wpLinkL10n.noTitle;
530 list += '</span><span class="item-info">' + this['info'] + '</span></li>';
535 this.ul[ firstPage ? 'html' : 'append' ]( list );
537 maybeLoad: function() {
540 bottom = el.scrollTop() + el.height();
542 if ( ! this.query.ready() || bottom < this.ul.height() - wpLink.riverBottomThreshold )
545 setTimeout(function() {
546 var newTop = el.scrollTop(),
547 newBottom = newTop + el.height();
549 if ( ! self.query.ready() || newBottom < self.ul.height() - wpLink.riverBottomThreshold )
553 el.scrollTop( newTop + self.waiting.outerHeight() );
555 self.ajax( function() { self.waiting.hide(); });
556 }, wpLink.timeToTriggerRiver );
560 Query = function( search ) {
562 this.allLoaded = false;
563 this.querying = false;
564 this.search = search;
567 $.extend( Query.prototype, {
569 return !( this.querying || this.allLoaded );
571 ajax: function( callback ) {
574 action : 'wp-link-ajax',
576 '_ajax_linking_nonce' : inputs.nonce.val()
580 query.search = this.search;
582 this.querying = true;
584 $.post( ajaxurl, query, function(r) {
586 self.querying = false;
588 callback( r, query );
593 $(document).ready( wpLink.init );