]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/wplink.js
WordPress 4.4-scripts
[autoinstalls/wordpress.git] / wp-includes / js / wplink.js
1 /* global ajaxurl, tinymce, wpLinkL10n, setUserSetting, wpActiveEditor */
2 var wpLink;
3
4 ( function( $ ) {
5         var editor, searchTimer, River, Query, correctedURL,
6                 inputs = {},
7                 rivers = {},
8                 isTouch = ( 'ontouchend' in document );
9
10         function getLink() {
11                 return editor.dom.getParent( editor.selection.getNode(), 'a' );
12         }
13
14         wpLink = {
15                 timeToTriggerRiver: 150,
16                 minRiverAJAXDuration: 200,
17                 riverBottomThreshold: 5,
18                 keySensitivity: 100,
19                 lastSearch: '',
20                 textarea: '',
21
22                 init: function() {
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' );
28
29                         // Input
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' );
35
36                         // Build Rivers
37                         rivers.search = new River( $( '#search-results' ) );
38                         rivers.recent = new River( $( '#most-recent-results' ) );
39                         rivers.elements = inputs.dialog.find( '.query-results' );
40
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' );
45
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();
51                                 wpLink.update();
52                         });
53                         inputs.close.add( inputs.backdrop ).add( '#wp-link-cancel a' ).click( function( event ) {
54                                 event.preventDefault();
55                                 wpLink.close();
56                         });
57
58                         $( '#wp-link-search-toggle' ).on( 'click', wpLink.toggleInternalLinking );
59
60                         rivers.elements.on( 'river-select', wpLink.updateFields );
61
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();
69                         } );
70
71                         inputs.search.on( 'keyup input', function() {
72                                 var self = this;
73
74                                 window.clearTimeout( searchTimer );
75                                 searchTimer = window.setTimeout( function() {
76                                         wpLink.searchInternalLinks.call( self );
77                                 }, 500 );
78                         });
79
80                         inputs.url.on( 'paste', function() {
81                                 setTimeout( wpLink.correctURL, 0 );
82                         } );
83
84                         inputs.url.on( 'blur', wpLink.correctURL );
85                 },
86
87                 // If URL wasn't corrected last time and doesn't start with http:, https:, ? # or /, prepend http://
88                 correctURL: function () {
89                         var url = $.trim( inputs.url.val() );
90
91                         if ( url && correctedURL !== url && ! /^(?:[a-z]+:|#|\?|\.|\/)/.test( url ) ) {
92                                 inputs.url.val( 'http://' + url );
93                                 correctedURL = url;
94                         }
95                 },
96
97                 open: function( editorId ) {
98                         var ed,
99                                 $body = $( document.body );
100
101                         $body.addClass( 'modal-open' );
102
103                         wpLink.range = null;
104
105                         if ( editorId ) {
106                                 window.wpActiveEditor = editorId;
107                         }
108
109                         if ( ! window.wpActiveEditor ) {
110                                 return;
111                         }
112
113                         this.textarea = $( '#' + window.wpActiveEditor ).get( 0 );
114
115                         if ( typeof tinymce !== 'undefined' ) {
116                                 // Make sure the link wrapper is the last element in the body,
117                                 // or the inline editor toolbar may show above the backdrop.
118                                 $body.append( inputs.backdrop, inputs.wrap );
119
120                                 ed = tinymce.get( wpActiveEditor );
121
122                                 if ( ed && ! ed.isHidden() ) {
123                                         editor = ed;
124                                 } else {
125                                         editor = null;
126                                 }
127
128                                 if ( editor && tinymce.isIE ) {
129                                         editor.windowManager.bookmark = editor.selection.getBookmark();
130                                 }
131                         }
132
133                         if ( ! wpLink.isMCE() && document.selection ) {
134                                 this.textarea.focus();
135                                 this.range = document.selection.createRange();
136                         }
137
138                         inputs.wrap.show();
139                         inputs.backdrop.show();
140
141                         wpLink.refresh();
142
143                         $( document ).trigger( 'wplink-open', inputs.wrap );
144                 },
145
146                 isMCE: function() {
147                         return editor && ! editor.isHidden();
148                 },
149
150                 refresh: function() {
151                         var text = '';
152
153                         // Refresh rivers (clear links, check visibility)
154                         rivers.search.refresh();
155                         rivers.recent.refresh();
156
157                         if ( wpLink.isMCE() ) {
158                                 wpLink.mceRefresh();
159                         } else {
160                                 // For the Text editor the "Link text" field is always shown
161                                 if ( ! inputs.wrap.hasClass( 'has-text-field' ) ) {
162                                         inputs.wrap.addClass( 'has-text-field' );
163                                 }
164
165                                 if ( document.selection ) {
166                                         // Old IE
167                                         text = document.selection.createRange().text || '';
168                                 } else if ( typeof this.textarea.selectionStart !== 'undefined' &&
169                                         ( this.textarea.selectionStart !== this.textarea.selectionEnd ) ) {
170                                         // W3C
171                                         text = this.textarea.value.substring( this.textarea.selectionStart, this.textarea.selectionEnd ) || '';
172                                 }
173
174                                 inputs.text.val( text );
175                                 wpLink.setDefaultValues();
176                         }
177
178                         if ( isTouch ) {
179                                 // Close the onscreen keyboard
180                                 inputs.url.focus().blur();
181                         } else {
182                                 // Focus the URL field and highlight its contents.
183                                 // If this is moved above the selection changes,
184                                 // IE will show a flashing cursor over the dialog.
185                                 inputs.url.focus()[0].select();
186                         }
187
188                         // Load the most recent results if this is the first time opening the panel.
189                         if ( ! rivers.recent.ul.children().length ) {
190                                 rivers.recent.ajax();
191                         }
192
193                         correctedURL = inputs.url.val().replace( /^http:\/\//, '' );
194                 },
195
196                 hasSelectedText: function( linkNode ) {
197                         var html = editor.selection.getContent();
198
199                         // Partial html and not a fully selected anchor element
200                         if ( /</.test( html ) && ( ! /^<a [^>]+>[^<]+<\/a>$/.test( html ) || html.indexOf('href=') === -1 ) ) {
201                                 return false;
202                         }
203
204                         if ( linkNode ) {
205                                 var nodes = linkNode.childNodes, i;
206
207                                 if ( nodes.length === 0 ) {
208                                         return false;
209                                 }
210
211                                 for ( i = nodes.length - 1; i >= 0; i-- ) {
212                                         if ( nodes[i].nodeType != 3 ) {
213                                                 return false;
214                                         }
215                                 }
216                         }
217
218                         return true;
219                 },
220
221                 mceRefresh: function() {
222                         var text,
223                                 selectedNode = editor.selection.getNode(),
224                                 linkNode = editor.dom.getParent( selectedNode, 'a[href]' ),
225                                 onlyText = this.hasSelectedText( linkNode );
226
227                         if ( linkNode ) {
228                                 text = linkNode.innerText || linkNode.textContent;
229                                 inputs.url.val( editor.dom.getAttrib( linkNode, 'href' ) );
230                                 inputs.openInNewTab.prop( 'checked', '_blank' === editor.dom.getAttrib( linkNode, 'target' ) );
231                                 inputs.submit.val( wpLinkL10n.update );
232                         } else {
233                                 text = editor.selection.getContent({ format: 'text' });
234                                 this.setDefaultValues();
235                         }
236
237                         if ( onlyText ) {
238                                 inputs.text.val( text || '' );
239                                 inputs.wrap.addClass( 'has-text-field' );
240                         } else {
241                                 inputs.text.val( '' );
242                                 inputs.wrap.removeClass( 'has-text-field' );
243                         }
244                 },
245
246                 close: function() {
247                         $( document.body ).removeClass( 'modal-open' );
248
249                         if ( ! wpLink.isMCE() ) {
250                                 wpLink.textarea.focus();
251
252                                 if ( wpLink.range ) {
253                                         wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
254                                         wpLink.range.select();
255                                 }
256                         } else {
257                                 editor.focus();
258                         }
259
260                         inputs.backdrop.hide();
261                         inputs.wrap.hide();
262
263                         correctedURL = false;
264
265                         $( document ).trigger( 'wplink-close', inputs.wrap );
266                 },
267
268                 getAttrs: function() {
269                         wpLink.correctURL();
270
271                         return {
272                                 href: $.trim( inputs.url.val() ),
273                                 target: inputs.openInNewTab.prop( 'checked' ) ? '_blank' : ''
274                         };
275                 },
276
277                 buildHtml: function(attrs) {
278                         var html = '<a href="' + attrs.href + '"';
279
280                         if ( attrs.target ) {
281                                 html += ' target="' + attrs.target + '"';
282                         }
283
284                         return html + '>';
285                 },
286
287                 update: function() {
288                         if ( wpLink.isMCE() ) {
289                                 wpLink.mceUpdate();
290                         } else {
291                                 wpLink.htmlUpdate();
292                         }
293                 },
294
295                 htmlUpdate: function() {
296                         var attrs, text, html, begin, end, cursor, selection,
297                                 textarea = wpLink.textarea;
298
299                         if ( ! textarea ) {
300                                 return;
301                         }
302
303                         attrs = wpLink.getAttrs();
304                         text = inputs.text.val();
305
306                         // If there's no href, return.
307                         if ( ! attrs.href ) {
308                                 return;
309                         }
310
311                         html = wpLink.buildHtml(attrs);
312
313                         // Insert HTML
314                         if ( document.selection && wpLink.range ) {
315                                 // IE
316                                 // Note: If no text is selected, IE will not place the cursor
317                                 //       inside the closing tag.
318                                 textarea.focus();
319                                 wpLink.range.text = html + ( text || wpLink.range.text ) + '</a>';
320                                 wpLink.range.moveToBookmark( wpLink.range.getBookmark() );
321                                 wpLink.range.select();
322
323                                 wpLink.range = null;
324                         } else if ( typeof textarea.selectionStart !== 'undefined' ) {
325                                 // W3C
326                                 begin = textarea.selectionStart;
327                                 end = textarea.selectionEnd;
328                                 selection = text || textarea.value.substring( begin, end );
329                                 html = html + selection + '</a>';
330                                 cursor = begin + html.length;
331
332                                 // If no text is selected, place the cursor inside the closing tag.
333                                 if ( begin === end && ! selection ) {
334                                         cursor -= 4;
335                                 }
336
337                                 textarea.value = (
338                                         textarea.value.substring( 0, begin ) +
339                                         html +
340                                         textarea.value.substring( end, textarea.value.length )
341                                 );
342
343                                 // Update cursor position
344                                 textarea.selectionStart = textarea.selectionEnd = cursor;
345                         }
346
347                         wpLink.close();
348                         textarea.focus();
349                 },
350
351                 mceUpdate: function() {
352                         var attrs = wpLink.getAttrs(),
353                                 link, text;
354
355                         wpLink.close();
356                         editor.focus();
357
358                         if ( tinymce.isIE ) {
359                                 editor.selection.moveToBookmark( editor.windowManager.bookmark );
360                         }
361
362                         if ( ! attrs.href ) {
363                                 editor.execCommand( 'unlink' );
364                                 return;
365                         }
366
367                         link = getLink();
368
369                         if ( inputs.wrap.hasClass( 'has-text-field' ) ) {
370                                 text = inputs.text.val() || attrs.href;
371                         }
372
373                         if ( link ) {
374                                 if ( text ) {
375                                         if ( 'innerText' in link ) {
376                                                 link.innerText = text;
377                                         } else {
378                                                 link.textContent = text;
379                                         }
380                                 }
381
382                                 editor.dom.setAttribs( link, attrs );
383                         } else {
384                                 if ( text ) {
385                                         editor.selection.setNode( editor.dom.create( 'a', attrs, editor.dom.encode( text ) ) );
386                                 } else {
387                                         editor.execCommand( 'mceInsertLink', false, attrs );
388                                 }
389                         }
390
391                         editor.nodeChanged();
392                 },
393
394                 updateFields: function( e, li ) {
395                         inputs.url.val( li.children( '.item-permalink' ).val() );
396                 },
397
398                 setDefaultValues: function() {
399                         var selection,
400                                 emailRegexp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
401                                 urlRegexp = /^(https?|ftp):\/\/[A-Z0-9.-]+\.[A-Z]{2,4}[^ "]*$/i;
402
403                         if ( this.isMCE() ) {
404                                 selection = editor.selection.getContent();
405                         } else if ( document.selection && wpLink.range ) {
406                                 selection = wpLink.range.text;
407                         } else if ( typeof this.textarea.selectionStart !== 'undefined' ) {
408                                 selection = this.textarea.value.substring( this.textarea.selectionStart, this.textarea.selectionEnd );
409                         }
410
411                         if ( selection && emailRegexp.test( selection ) ) {
412                                 // Selection is email address
413                                 inputs.url.val( 'mailto:' + selection );
414                         } else if ( selection && urlRegexp.test( selection ) ) {
415                                 // Selection is URL
416                                 inputs.url.val( selection.replace( /&amp;|&#0?38;/gi, '&' ) );
417                         } else {
418                                 // Set URL to default.
419                                 inputs.url.val( '' );
420                         }
421
422                         // Update save prompt.
423                         inputs.submit.val( wpLinkL10n.save );
424                 },
425
426                 searchInternalLinks: function() {
427                         var t = $( this ), waiting,
428                                 search = t.val();
429
430                         if ( search.length > 2 ) {
431                                 rivers.recent.hide();
432                                 rivers.search.show();
433
434                                 // Don't search if the keypress didn't change the title.
435                                 if ( wpLink.lastSearch == search )
436                                         return;
437
438                                 wpLink.lastSearch = search;
439                                 waiting = t.parent().find( '.spinner' ).addClass( 'is-active' );
440
441                                 rivers.search.change( search );
442                                 rivers.search.ajax( function() {
443                                         waiting.removeClass( 'is-active' );
444                                 });
445                         } else {
446                                 rivers.search.hide();
447                                 rivers.recent.show();
448                         }
449                 },
450
451                 next: function() {
452                         rivers.search.next();
453                         rivers.recent.next();
454                 },
455
456                 prev: function() {
457                         rivers.search.prev();
458                         rivers.recent.prev();
459                 },
460
461                 keydown: function( event ) {
462                         var fn, id;
463
464                         // Escape key.
465                         if ( 27 === event.keyCode ) {
466                                 wpLink.close();
467                                 event.stopImmediatePropagation();
468                         // Tab key.
469                         } else if ( 9 === event.keyCode ) {
470                                 id = event.target.id;
471
472                                 // wp-link-submit must always be the last focusable element in the dialog.
473                                 // following focusable elements will be skipped on keyboard navigation.
474                                 if ( id === 'wp-link-submit' && ! event.shiftKey ) {
475                                         inputs.close.focus();
476                                         event.preventDefault();
477                                 } else if ( id === 'wp-link-close' && event.shiftKey ) {
478                                         inputs.submit.focus();
479                                         event.preventDefault();
480                                 }
481                         }
482
483                         // Up Arrow and Down Arrow keys.
484                         if ( 38 !== event.keyCode && 40 !== event.keyCode ) {
485                                 return;
486                         }
487
488                         if ( document.activeElement &&
489                                 ( document.activeElement.id === 'link-title-field' || document.activeElement.id === 'url-field' ) ) {
490                                 return;
491                         }
492
493                         // Up Arrow key.
494                         fn = 38 === event.keyCode ? 'prev' : 'next';
495                         clearInterval( wpLink.keyInterval );
496                         wpLink[ fn ]();
497                         wpLink.keyInterval = setInterval( wpLink[ fn ], wpLink.keySensitivity );
498                         event.preventDefault();
499                 },
500
501                 keyup: function( event ) {
502                         // Up Arrow and Down Arrow keys.
503                         if ( 38 === event.keyCode || 40 === event.keyCode ) {
504                                 clearInterval( wpLink.keyInterval );
505                                 event.preventDefault();
506                         }
507                 },
508
509                 delayedCallback: function( func, delay ) {
510                         var timeoutTriggered, funcTriggered, funcArgs, funcContext;
511
512                         if ( ! delay )
513                                 return func;
514
515                         setTimeout( function() {
516                                 if ( funcTriggered )
517                                         return func.apply( funcContext, funcArgs );
518                                 // Otherwise, wait.
519                                 timeoutTriggered = true;
520                         }, delay );
521
522                         return function() {
523                                 if ( timeoutTriggered )
524                                         return func.apply( this, arguments );
525                                 // Otherwise, wait.
526                                 funcArgs = arguments;
527                                 funcContext = this;
528                                 funcTriggered = true;
529                         };
530                 },
531
532                 toggleInternalLinking: function( event ) {
533                         var visible = inputs.wrap.hasClass( 'search-panel-visible' );
534
535                         inputs.wrap.toggleClass( 'search-panel-visible', ! visible );
536                         setUserSetting( 'wplink', visible ? '0' : '1' );
537                         inputs[ ! visible ? 'search' : 'url' ].focus();
538                         event.preventDefault();
539                 }
540         };
541
542         River = function( element, search ) {
543                 var self = this;
544                 this.element = element;
545                 this.ul = element.children( 'ul' );
546                 this.contentHeight = element.children( '#link-selector-height' );
547                 this.waiting = element.find('.river-waiting');
548
549                 this.change( search );
550                 this.refresh();
551
552                 $( '#wp-link .query-results, #wp-link #link-selector' ).scroll( function() {
553                         self.maybeLoad();
554                 });
555                 element.on( 'click', 'li', function( event ) {
556                         self.select( $( this ), event );
557                 });
558         };
559
560         $.extend( River.prototype, {
561                 refresh: function() {
562                         this.deselect();
563                         this.visible = this.element.is( ':visible' );
564                 },
565                 show: function() {
566                         if ( ! this.visible ) {
567                                 this.deselect();
568                                 this.element.show();
569                                 this.visible = true;
570                         }
571                 },
572                 hide: function() {
573                         this.element.hide();
574                         this.visible = false;
575                 },
576                 // Selects a list item and triggers the river-select event.
577                 select: function( li, event ) {
578                         var liHeight, elHeight, liTop, elTop;
579
580                         if ( li.hasClass( 'unselectable' ) || li == this.selected )
581                                 return;
582
583                         this.deselect();
584                         this.selected = li.addClass( 'selected' );
585                         // Make sure the element is visible
586                         liHeight = li.outerHeight();
587                         elHeight = this.element.height();
588                         liTop = li.position().top;
589                         elTop = this.element.scrollTop();
590
591                         if ( liTop < 0 ) // Make first visible element
592                                 this.element.scrollTop( elTop + liTop );
593                         else if ( liTop + liHeight > elHeight ) // Make last visible element
594                                 this.element.scrollTop( elTop + liTop - elHeight + liHeight );
595
596                         // Trigger the river-select event
597                         this.element.trigger( 'river-select', [ li, event, this ] );
598                 },
599                 deselect: function() {
600                         if ( this.selected )
601                                 this.selected.removeClass( 'selected' );
602                         this.selected = false;
603                 },
604                 prev: function() {
605                         if ( ! this.visible )
606                                 return;
607
608                         var to;
609                         if ( this.selected ) {
610                                 to = this.selected.prev( 'li' );
611                                 if ( to.length )
612                                         this.select( to );
613                         }
614                 },
615                 next: function() {
616                         if ( ! this.visible )
617                                 return;
618
619                         var to = this.selected ? this.selected.next( 'li' ) : $( 'li:not(.unselectable):first', this.element );
620                         if ( to.length )
621                                 this.select( to );
622                 },
623                 ajax: function( callback ) {
624                         var self = this,
625                                 delay = this.query.page == 1 ? 0 : wpLink.minRiverAJAXDuration,
626                                 response = wpLink.delayedCallback( function( results, params ) {
627                                         self.process( results, params );
628                                         if ( callback )
629                                                 callback( results, params );
630                                 }, delay );
631
632                         this.query.ajax( response );
633                 },
634                 change: function( search ) {
635                         if ( this.query && this._search == search )
636                                 return;
637
638                         this._search = search;
639                         this.query = new Query( search );
640                         this.element.scrollTop( 0 );
641                 },
642                 process: function( results, params ) {
643                         var list = '', alt = true, classes = '',
644                                 firstPage = params.page == 1;
645
646                         if ( ! results ) {
647                                 if ( firstPage ) {
648                                         list += '<li class="unselectable no-matches-found"><span class="item-title"><em>' +
649                                                 wpLinkL10n.noMatchesFound + '</em></span></li>';
650                                 }
651                         } else {
652                                 $.each( results, function() {
653                                         classes = alt ? 'alternate' : '';
654                                         classes += this.title ? '' : ' no-title';
655                                         list += classes ? '<li class="' + classes + '">' : '<li>';
656                                         list += '<input type="hidden" class="item-permalink" value="' + this.permalink + '" />';
657                                         list += '<span class="item-title">';
658                                         list += this.title ? this.title : wpLinkL10n.noTitle;
659                                         list += '</span><span class="item-info">' + this.info + '</span></li>';
660                                         alt = ! alt;
661                                 });
662                         }
663
664                         this.ul[ firstPage ? 'html' : 'append' ]( list );
665                 },
666                 maybeLoad: function() {
667                         var self = this,
668                                 el = this.element,
669                                 bottom = el.scrollTop() + el.height();
670
671                         if ( ! this.query.ready() || bottom < this.contentHeight.height() - wpLink.riverBottomThreshold )
672                                 return;
673
674                         setTimeout(function() {
675                                 var newTop = el.scrollTop(),
676                                         newBottom = newTop + el.height();
677
678                                 if ( ! self.query.ready() || newBottom < self.contentHeight.height() - wpLink.riverBottomThreshold )
679                                         return;
680
681                                 self.waiting.addClass( 'is-active' );
682                                 el.scrollTop( newTop + self.waiting.outerHeight() );
683
684                                 self.ajax( function() {
685                                         self.waiting.removeClass( 'is-active' );
686                                 });
687                         }, wpLink.timeToTriggerRiver );
688                 }
689         });
690
691         Query = function( search ) {
692                 this.page = 1;
693                 this.allLoaded = false;
694                 this.querying = false;
695                 this.search = search;
696         };
697
698         $.extend( Query.prototype, {
699                 ready: function() {
700                         return ! ( this.querying || this.allLoaded );
701                 },
702                 ajax: function( callback ) {
703                         var self = this,
704                                 query = {
705                                         action : 'wp-link-ajax',
706                                         page : this.page,
707                                         '_ajax_linking_nonce' : inputs.nonce.val()
708                                 };
709
710                         if ( this.search )
711                                 query.search = this.search;
712
713                         this.querying = true;
714
715                         $.post( ajaxurl, query, function( r ) {
716                                 self.page++;
717                                 self.querying = false;
718                                 self.allLoaded = ! r;
719                                 callback( r, query );
720                         }, 'json' );
721                 }
722         });
723
724         $( document ).ready( wpLink.init );
725 })( jQuery );