1 /* global ajaxurl, tinymce, wpLinkL10n, setUserSetting, wpActiveEditor */
5 var editor, searchTimer, River, Query, correctedURL,
8 isTouch = ( 'ontouchend' in document );
11 return editor.dom.getParent( editor.selection.getNode(), 'a' );
15 timeToTriggerRiver: 150,
16 minRiverAJAXDuration: 200,
17 riverBottomThreshold: 5,
23 inputs.wrap = $('#wp-link-wrap');
24 inputs.dialog = $( '#wp-link' );
25 inputs.backdrop = $( '#wp-link-backdrop' );
26 inputs.submit = $( '#wp-link-submit' );
27 inputs.close = $( '#wp-link-close' );
30 inputs.text = $( '#wp-link-text' );
31 inputs.url = $( '#wp-link-url' );
32 inputs.nonce = $( '#_ajax_linking_nonce' );
33 inputs.openInNewTab = $( '#wp-link-target' );
34 inputs.search = $( '#wp-link-search' );
37 rivers.search = new River( $( '#search-results' ) );
38 rivers.recent = new River( $( '#most-recent-results' ) );
39 rivers.elements = inputs.dialog.find( '.query-results' );
41 // Get search notice text
42 inputs.queryNotice = $( '#query-notice-message' );
43 inputs.queryNoticeTextDefault = inputs.queryNotice.find( '.query-notice-default' );
44 inputs.queryNoticeTextHint = inputs.queryNotice.find( '.query-notice-hint' );
46 // Bind event handlers
47 inputs.dialog.keydown( wpLink.keydown );
48 inputs.dialog.keyup( wpLink.keyup );
49 inputs.submit.click( function( event ) {
50 event.preventDefault();
53 inputs.close.add( inputs.backdrop ).add( '#wp-link-cancel a' ).click( function( event ) {
54 event.preventDefault();
58 $( '#wp-link-search-toggle' ).on( 'click', wpLink.toggleInternalLinking );
60 rivers.elements.on( 'river-select', wpLink.updateFields );
62 // Display 'hint' message when search field or 'query-results' box are focused
63 inputs.search.on( 'focus.wplink', function() {
64 inputs.queryNoticeTextDefault.hide();
65 inputs.queryNoticeTextHint.removeClass( 'screen-reader-text' ).show();
66 } ).on( 'blur.wplink', function() {
67 inputs.queryNoticeTextDefault.show();
68 inputs.queryNoticeTextHint.addClass( 'screen-reader-text' ).hide();
71 inputs.search.keyup( function() {
74 window.clearTimeout( searchTimer );
75 searchTimer = window.setTimeout( function() {
76 wpLink.searchInternalLinks.call( self );
80 function correctURL() {
81 var url = $.trim( inputs.url.val() );
83 if ( url && correctedURL !== url && ! /^(?:[a-z]+:|#|\?|\.|\/)/.test( url ) ) {
84 inputs.url.val( 'http://' + url );
89 inputs.url.on( 'paste', function() {
90 setTimeout( correctURL, 0 );
93 inputs.url.on( 'blur', correctURL );
96 open: function( editorId ) {
99 $( document.body ).addClass( 'modal-open' );
104 window.wpActiveEditor = editorId;
107 if ( ! window.wpActiveEditor ) {
111 this.textarea = $( '#' + window.wpActiveEditor ).get( 0 );
113 if ( typeof tinymce !== 'undefined' ) {
114 ed = tinymce.get( wpActiveEditor );
116 if ( ed && ! ed.isHidden() ) {
122 if ( editor && tinymce.isIE ) {
123 editor.windowManager.bookmark = editor.selection.getBookmark();
127 if ( ! wpLink.isMCE() && document.selection ) {
128 this.textarea.focus();
129 this.range = document.selection.createRange();
133 inputs.backdrop.show();
137 $( document ).trigger( 'wplink-open', inputs.wrap );
141 return editor && ! editor.isHidden();
144 refresh: function() {
147 // Refresh rivers (clear links, check visibility)
148 rivers.search.refresh();
149 rivers.recent.refresh();
151 if ( wpLink.isMCE() ) {
154 // For the Text editor the "Link text" field is always shown
155 if ( ! inputs.wrap.hasClass( 'has-text-field' ) ) {
156 inputs.wrap.addClass( 'has-text-field' );
159 if ( document.selection ) {
161 text = document.selection.createRange().text || '';
162 } else if ( typeof this.textarea.selectionStart !== 'undefined' &&
163 ( this.textarea.selectionStart !== this.textarea.selectionEnd ) ) {
165 text = this.textarea.value.substring( this.textarea.selectionStart, this.textarea.selectionEnd ) || '';
168 inputs.text.val( text );
169 wpLink.setDefaultValues();
173 // Close the onscreen keyboard
174 inputs.url.focus().blur();
176 // Focus the URL field and highlight its contents.
177 // If this is moved above the selection changes,
178 // IE will show a flashing cursor over the dialog.
179 inputs.url.focus()[0].select();
182 // Load the most recent results if this is the first time opening the panel.
183 if ( ! rivers.recent.ul.children().length ) {
184 rivers.recent.ajax();
187 correctedURL = inputs.url.val().replace( /^http:\/\//, '' );
190 hasSelectedText: function( linkNode ) {
191 var html = editor.selection.getContent();
193 // Partial html and not a fully selected anchor element
194 if ( /</.test( html ) && ( ! /^<a [^>]+>[^<]+<\/a>$/.test( html ) || html.indexOf('href=') === -1 ) ) {
199 var nodes = linkNode.childNodes, i;
201 if ( nodes.length === 0 ) {
205 for ( i = nodes.length - 1; i >= 0; i-- ) {
206 if ( nodes[i].nodeType != 3 ) {
215 mceRefresh: function() {
217 selectedNode = editor.selection.getNode(),
218 linkNode = editor.dom.getParent( selectedNode, 'a[href]' ),
219 onlyText = this.hasSelectedText( linkNode );
222 text = linkNode.innerText || linkNode.textContent;
223 inputs.url.val( editor.dom.getAttrib( linkNode, 'href' ) );
224 inputs.openInNewTab.prop( 'checked', '_blank' === editor.dom.getAttrib( linkNode, 'target' ) );
225 inputs.submit.val( wpLinkL10n.update );
227 text = editor.selection.getContent({ format: 'text' });
228 this.setDefaultValues();
232 inputs.text.val( text || '' );
233 inputs.wrap.addClass( 'has-text-field' );
235 inputs.text.val( '' );
236 inputs.wrap.removeClass( 'has-text-field' );
241 $( document.body ).removeClass( 'modal-open' );
243 if ( ! wpLink.isMCE() ) {
244 wpLink.textarea.focus();
246 if ( wpLink.range ) {
247 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
248 wpLink.range.select();
254 inputs.backdrop.hide();
257 correctedURL = false;
259 $( document ).trigger( 'wplink-close', inputs.wrap );
262 getAttrs: function() {
264 href: $.trim( inputs.url.val() ),
265 target: inputs.openInNewTab.prop( 'checked' ) ? '_blank' : ''
270 if ( wpLink.isMCE() ) {
277 htmlUpdate: function() {
278 var attrs, text, html, begin, end, cursor, selection,
279 textarea = wpLink.textarea;
285 attrs = wpLink.getAttrs();
286 text = inputs.text.val();
288 // If there's no href, return.
289 if ( ! attrs.href ) {
294 html = '<a href="' + attrs.href + '"';
296 if ( attrs.target ) {
297 html += ' target="' + attrs.target + '"';
303 if ( document.selection && wpLink.range ) {
305 // Note: If no text is selected, IE will not place the cursor
306 // inside the closing tag.
308 wpLink.range.text = html + ( text || wpLink.range.text ) + '</a>';
309 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
310 wpLink.range.select();
313 } else if ( typeof textarea.selectionStart !== 'undefined' ) {
315 begin = textarea.selectionStart;
316 end = textarea.selectionEnd;
317 selection = text || textarea.value.substring( begin, end );
318 html = html + selection + '</a>';
319 cursor = begin + html.length;
321 // If no text is selected, place the cursor inside the closing tag.
322 if ( begin === end && ! selection ) {
327 textarea.value.substring( 0, begin ) +
329 textarea.value.substring( end, textarea.value.length )
332 // Update cursor position
333 textarea.selectionStart = textarea.selectionEnd = cursor;
340 mceUpdate: function() {
341 var attrs = wpLink.getAttrs(),
347 if ( tinymce.isIE ) {
348 editor.selection.moveToBookmark( editor.windowManager.bookmark );
351 if ( ! attrs.href ) {
352 editor.execCommand( 'unlink' );
357 text = inputs.text.val();
361 if ( 'innerText' in link ) {
362 link.innerText = text;
364 link.textContent = text;
368 editor.dom.setAttribs( link, attrs );
371 editor.selection.setNode( editor.dom.create( 'a', attrs, text ) );
373 editor.execCommand( 'mceInsertLink', false, attrs );
378 updateFields: function( e, li ) {
379 inputs.url.val( li.children( '.item-permalink' ).val() );
382 setDefaultValues: function() {
384 emailRegexp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
385 urlRegexp = /^(https?|ftp):\/\/[A-Z0-9.-]+\.[A-Z]{2,4}[^ "]*$/i;
387 if ( this.isMCE() ) {
388 selection = editor.selection.getContent();
389 } else if ( document.selection && wpLink.range ) {
390 selection = wpLink.range.text;
391 } else if ( typeof this.textarea.selectionStart !== 'undefined' ) {
392 selection = this.textarea.value.substring( this.textarea.selectionStart, this.textarea.selectionEnd );
395 if ( selection && emailRegexp.test( selection ) ) {
396 // Selection is email address
397 inputs.url.val( 'mailto:' + selection );
398 } else if ( selection && urlRegexp.test( selection ) ) {
400 inputs.url.val( selection.replace( /&|�?38;/gi, '&' ) );
402 // Set URL to default.
403 inputs.url.val( '' );
406 // Update save prompt.
407 inputs.submit.val( wpLinkL10n.save );
410 searchInternalLinks: function() {
411 var t = $( this ), waiting,
414 if ( search.length > 2 ) {
415 rivers.recent.hide();
416 rivers.search.show();
418 // Don't search if the keypress didn't change the title.
419 if ( wpLink.lastSearch == search )
422 wpLink.lastSearch = search;
423 waiting = t.parent().find( '.spinner' ).addClass( 'is-active' );
425 rivers.search.change( search );
426 rivers.search.ajax( function() {
427 waiting.removeClass( 'is-active' );
430 rivers.search.hide();
431 rivers.recent.show();
436 rivers.search.next();
437 rivers.recent.next();
441 rivers.search.prev();
442 rivers.recent.prev();
445 keydown: function( event ) {
449 if ( key.ESCAPE === event.keyCode ) {
451 event.stopImmediatePropagation();
452 } else if ( key.TAB === event.keyCode ) {
453 id = event.target.id;
455 // wp-link-submit must always be the last focusable element in the dialog.
456 // following focusable elements will be skipped on keyboard navigation.
457 if ( id === 'wp-link-submit' && ! event.shiftKey ) {
458 inputs.close.focus();
459 event.preventDefault();
460 } else if ( id === 'wp-link-close' && event.shiftKey ) {
461 inputs.submit.focus();
462 event.preventDefault();
466 if ( event.keyCode !== key.UP && event.keyCode !== key.DOWN ) {
470 if ( document.activeElement &&
471 ( document.activeElement.id === 'link-title-field' || document.activeElement.id === 'url-field' ) ) {
475 fn = event.keyCode === key.UP ? 'prev' : 'next';
476 clearInterval( wpLink.keyInterval );
478 wpLink.keyInterval = setInterval( wpLink[ fn ], wpLink.keySensitivity );
479 event.preventDefault();
482 keyup: function( event ) {
483 var key = $.ui.keyCode;
485 if ( event.which === key.UP || event.which === key.DOWN ) {
486 clearInterval( wpLink.keyInterval );
487 event.preventDefault();
491 delayedCallback: function( func, delay ) {
492 var timeoutTriggered, funcTriggered, funcArgs, funcContext;
497 setTimeout( function() {
499 return func.apply( funcContext, funcArgs );
501 timeoutTriggered = true;
505 if ( timeoutTriggered )
506 return func.apply( this, arguments );
508 funcArgs = arguments;
510 funcTriggered = true;
514 toggleInternalLinking: function( event ) {
515 var visible = inputs.wrap.hasClass( 'search-panel-visible' );
517 inputs.wrap.toggleClass( 'search-panel-visible', ! visible );
518 setUserSetting( 'wplink', visible ? '0' : '1' );
519 inputs[ ! visible ? 'search' : 'url' ].focus();
520 event.preventDefault();
524 River = function( element, search ) {
526 this.element = element;
527 this.ul = element.children( 'ul' );
528 this.contentHeight = element.children( '#link-selector-height' );
529 this.waiting = element.find('.river-waiting');
531 this.change( search );
534 $( '#wp-link .query-results, #wp-link #link-selector' ).scroll( function() {
537 element.on( 'click', 'li', function( event ) {
538 self.select( $( this ), event );
542 $.extend( River.prototype, {
543 refresh: function() {
545 this.visible = this.element.is( ':visible' );
548 if ( ! this.visible ) {
556 this.visible = false;
558 // Selects a list item and triggers the river-select event.
559 select: function( li, event ) {
560 var liHeight, elHeight, liTop, elTop;
562 if ( li.hasClass( 'unselectable' ) || li == this.selected )
566 this.selected = li.addClass( 'selected' );
567 // Make sure the element is visible
568 liHeight = li.outerHeight();
569 elHeight = this.element.height();
570 liTop = li.position().top;
571 elTop = this.element.scrollTop();
573 if ( liTop < 0 ) // Make first visible element
574 this.element.scrollTop( elTop + liTop );
575 else if ( liTop + liHeight > elHeight ) // Make last visible element
576 this.element.scrollTop( elTop + liTop - elHeight + liHeight );
578 // Trigger the river-select event
579 this.element.trigger( 'river-select', [ li, event, this ] );
581 deselect: function() {
583 this.selected.removeClass( 'selected' );
584 this.selected = false;
587 if ( ! this.visible )
591 if ( this.selected ) {
592 to = this.selected.prev( 'li' );
598 if ( ! this.visible )
601 var to = this.selected ? this.selected.next( 'li' ) : $( 'li:not(.unselectable):first', this.element );
605 ajax: function( callback ) {
607 delay = this.query.page == 1 ? 0 : wpLink.minRiverAJAXDuration,
608 response = wpLink.delayedCallback( function( results, params ) {
609 self.process( results, params );
611 callback( results, params );
614 this.query.ajax( response );
616 change: function( search ) {
617 if ( this.query && this._search == search )
620 this._search = search;
621 this.query = new Query( search );
622 this.element.scrollTop( 0 );
624 process: function( results, params ) {
625 var list = '', alt = true, classes = '',
626 firstPage = params.page == 1;
630 list += '<li class="unselectable no-matches-found"><span class="item-title"><em>' +
631 wpLinkL10n.noMatchesFound + '</em></span></li>';
634 $.each( results, function() {
635 classes = alt ? 'alternate' : '';
636 classes += this.title ? '' : ' no-title';
637 list += classes ? '<li class="' + classes + '">' : '<li>';
638 list += '<input type="hidden" class="item-permalink" value="' + this.permalink + '" />';
639 list += '<span class="item-title">';
640 list += this.title ? this.title : wpLinkL10n.noTitle;
641 list += '</span><span class="item-info">' + this.info + '</span></li>';
646 this.ul[ firstPage ? 'html' : 'append' ]( list );
648 maybeLoad: function() {
651 bottom = el.scrollTop() + el.height();
653 if ( ! this.query.ready() || bottom < this.contentHeight.height() - wpLink.riverBottomThreshold )
656 setTimeout(function() {
657 var newTop = el.scrollTop(),
658 newBottom = newTop + el.height();
660 if ( ! self.query.ready() || newBottom < self.contentHeight.height() - wpLink.riverBottomThreshold )
663 self.waiting.addClass( 'is-active' );
664 el.scrollTop( newTop + self.waiting.outerHeight() );
666 self.ajax( function() {
667 self.waiting.removeClass( 'is-active' );
669 }, wpLink.timeToTriggerRiver );
673 Query = function( search ) {
675 this.allLoaded = false;
676 this.querying = false;
677 this.search = search;
680 $.extend( Query.prototype, {
682 return ! ( this.querying || this.allLoaded );
684 ajax: function( callback ) {
687 action : 'wp-link-ajax',
689 '_ajax_linking_nonce' : inputs.nonce.val()
693 query.search = this.search;
695 this.querying = true;
697 $.post( ajaxurl, query, function( r ) {
699 self.querying = false;
700 self.allLoaded = ! r;
701 callback( r, query );
706 $( document ).ready( wpLink.init );