diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 00000000..66a218b5 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,2 @@ +# upstream lib from Google +modules/contentCollector.js diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..64cd5087 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,9 @@ +{ + "predef": [ + "mediaWiki", + "jQuery" + ], + "browser": true, + "smarttabs": true, + "multistr": true +} diff --git a/modules/ext.wikiEditor.css b/modules/ext.wikiEditor.css index 53e320d5..f0665730 100644 --- a/modules/ext.wikiEditor.css +++ b/modules/ext.wikiEditor.css @@ -1,4 +1,4 @@ -/* +/** * CSS for WikiEditor */ @@ -7,10 +7,13 @@ form#editform { margin: 0; padding: 0; } + /* These IDs (#wpSummaryLabel and #wpSummary) could change in MediaWiki */ -#wpSummary, #wpSummaryLabel { +#wpSummary, +#wpSummaryLabel { margin-bottom: 1em; } + /* This ID (#wpTextbox1) could change in MediaWiki */ .wikiEditor-ui textarea#wpTextbox1 { border: none; @@ -19,6 +22,7 @@ form#editform { line-height: 1.5em; resize: vertical; } + .wikiEditor-ui .wikiEditor-ui-text > textarea#wpTextbox1 { margin: 0; } diff --git a/modules/ext.wikiEditor.dialogs.js b/modules/ext.wikiEditor.dialogs.js index 45a19593..0e0dd090 100644 --- a/modules/ext.wikiEditor.dialogs.js +++ b/modules/ext.wikiEditor.dialogs.js @@ -12,4 +12,4 @@ $( document ).ready( function() { // Add dialogs module $( '#wpTextbox1' ).wikiEditor( 'addModule', $.wikiEditor.modules.dialogs.config.getDefaultConfig() ); -} ); \ No newline at end of file +} ); diff --git a/modules/ext.wikiEditor.templateEditor.js b/modules/ext.wikiEditor.templateEditor.js index 650910d2..8879d10d 100644 --- a/modules/ext.wikiEditor.templateEditor.js +++ b/modules/ext.wikiEditor.templateEditor.js @@ -2,9 +2,9 @@ * JavaScript for WikiEditor Template Editor */ -$( document ).ready( function() { +$( document ).ready( function () { // Disable in template namespace - if ( mw.config.get( 'wgNamespaceNumber' ) == 10 ) { + if ( mw.config.get( 'wgNamespaceNumber' ) === 10 ) { return true; } // Add template editor module diff --git a/modules/ext.wikiEditor.templates.js b/modules/ext.wikiEditor.templates.js index 4ac09b54..6eae6de5 100644 --- a/modules/ext.wikiEditor.templates.js +++ b/modules/ext.wikiEditor.templates.js @@ -2,11 +2,11 @@ * JavaScript for WikiEditor Templates */ -$( document ).ready( function() { +$( document ).ready( function () { // Disable for template namespace - if ( mw.config.get( 'wgNamespaceNumber' ) == 10 ) { + if ( mw.config.get( 'wgNamespaceNumber' ) === 10 ) { return true; } // Add templates module $( '#wpTextbox1' ).wikiEditor( 'addModule', 'templates' ); -} ); \ No newline at end of file +} ); diff --git a/modules/ext.wikiEditor.tests.toolbar.js b/modules/ext.wikiEditor.tests.toolbar.js index 69fe2905..1c5a67d1 100644 --- a/modules/ext.wikiEditor.tests.toolbar.js +++ b/modules/ext.wikiEditor.tests.toolbar.js @@ -4,9 +4,9 @@ var textareaId = '#wpTextbox1'; var wikiEditorTests = { // Add emoticons section - 'add_sections_toolbar': { - 'call': 'addToToolbar', - 'data': { + add_sections_toolbar: { + call: 'addToToolbar', + data: { 'sections': { 'emoticons': { 'type': 'toolbar', @@ -14,30 +14,30 @@ var wikiEditorTests = { } } }, - 'test': '*[rel=emoticons].section', - 'pre': 0, - 'post': 1 + test: '*[rel=emoticons].section', + pre: 0, + post: 1 }, // Add faces group to emoticons section 'add_groups': { - 'call': 'addToToolbar', - 'data': { - 'section': 'emoticons', + call: 'addToToolbar', + data: { + section: 'emoticons', 'groups': { 'faces': { 'label': 'Faces' } } }, - 'test': '*[rel=emoticons].section *[rel=faces].group', - 'pre': 0, - 'post': 1 + test: '*[rel=emoticons].section *[rel=faces].group', + pre: 0, + post: 1 }, // Add smile tool to faces group of emoticons section 'add_tools': { - 'call': 'addToToolbar', - 'data': { - 'section': 'emoticons', + call: 'addToToolbar', + data: { + section: 'emoticons', 'group': 'faces', 'tools': { 'smile': { @@ -47,20 +47,20 @@ var wikiEditorTests = { action: { type: 'encapsulate', options: { - pre: ":)" + pre: ':)' } } } } }, - 'test': '*[rel=emoticons].section *[rel=faces].group *[rel=smile].tool', - 'pre': 0, - 'post': 1 + test: '*[rel=emoticons].section *[rel=faces].group *[rel=smile].tool', + pre: 0, + post: 1 }, // Add info section 'add_sections_booklet': { - 'call': 'addToToolbar', - 'data': { + call: 'addToToolbar', + data: { 'sections': { 'info': { 'type': 'booklet', @@ -68,16 +68,16 @@ var wikiEditorTests = { } } }, - 'test': '*[rel=info].section', - 'pre': 0, - 'post': 1 + test: '*[rel=info].section', + pre: 0, + post: 1 }, // Add info section 'add_pages_table': { - 'call': 'addToToolbar', - 'data': { - 'section': 'info', - 'pages': { + call: 'addToToolbar', + data: { + section: 'info', + pages: { 'colors': { 'layout': 'table', 'label': 'Colors', @@ -89,44 +89,44 @@ var wikiEditorTests = { } } }, - 'test': '*[rel=info].section *[rel=colors].page', - 'pre': 0, - 'post': 1 + test: '*[rel=info].section *[rel=colors].page', + pre: 0, + post: 1 }, // Add colors rows 'add_rows': { - 'call': 'addToToolbar', - 'data': { - 'section': 'info', - 'page': 'colors', + call: 'addToToolbar', + data: { + section: 'info', + page: 'colors', 'rows': [ { 'name': { text: 'Red' }, 'temp': { text: 'Warm' }, - 'swatch': { html: '
' } + 'swatch': { html: '
' } }, { 'name': { text: 'Blue' }, 'temp': { text: 'Cold' }, - 'swatch': { html: '
' } + 'swatch': { html: '
' } }, { 'name': { text: 'Silver' }, 'temp': { text: 'Neutral' }, - 'swatch': { html: '
' } + 'swatch': { html: '
' } } ] }, - 'test': '*[rel=info].section *[rel=colors].page tr td', - 'pre': 0, - 'post': 9 + test: '*[rel=info].section *[rel=colors].page tr td', + pre: 0, + post: 9 }, // Add 'add_pages_characters': { - 'call': 'addToToolbar', - 'data': { - 'section': 'info', - 'pages': { + call: 'addToToolbar', + data: { + section: 'info', + pages: { 'emoticons': { 'layout': 'characters', 'label': 'Emoticons' @@ -137,110 +137,118 @@ var wikiEditorTests = { } } }, - 'test': '*[rel=info].section *[rel=emoticons].page', - 'pre': 0, - 'post': 1 + test: '*[rel=info].section *[rel=emoticons].page', + pre: 0, + post: 1 }, // Add 'add_characters': { - 'call': 'addToToolbar', - 'data': { - 'section': 'info', - 'page': 'emoticons', - 'characters': [ ':)', ':))', ':(', '<3', ';)' ] + call: 'addToToolbar', + data: { + section: 'info', + page: 'emoticons', + characters: [ ':)', ':))', ':(', '<3', ';)' ] }, - 'test': '*[rel=info].section *[rel=emoticons].page *[rel=":)"]', - 'pre': 0, - 'post': 1 + test: '*[rel=info].section *[rel=emoticons].page *[rel=":)"]', + pre: 0, + post: 1 }, // Remove page 'remove_page': { - 'call': 'removeFromToolbar', - 'data': { - 'section': 'info', - 'page': 'removeme' + call: 'removeFromToolbar', + data: { + section: 'info', + page: 'removeme' }, - 'test': '*[rel=info].section *[rel=removeme].page', - 'pre': 1, - 'post': 0 + test: '*[rel=info].section *[rel=removeme].page', + pre: 1, + post: 0 }, // Remove :)) from emoticon characters 'remove_character': { - 'call': 'removeFromToolbar', - 'data': { - 'section': 'info', - 'page': 'emoticons', + call: 'removeFromToolbar', + data: { + section: 'info', + page: 'emoticons', 'character': ':))' }, - 'test': '*[rel=info].section *[rel=emoticons].page *[rel=":))"]', - 'pre': 1, - 'post': 0 + test: '*[rel=info].section *[rel=emoticons].page *[rel=":))"]', + pre: 1, + post: 0 }, // Remove row from colors table of info section 'remove_row': { - 'call': 'removeFromToolbar', - 'data': { - 'section': 'info', - 'page': 'colors', + call: 'removeFromToolbar', + data: { + section: 'info', + page: 'colors', 'row': 0 }, - 'test': '*[rel=info].section *[rel=colors].page tr td', - 'pre': 9, - 'post': 6 + test: '*[rel=info].section *[rel=colors].page tr td', + pre: 9, + post: 6 } }; -$(document).ready( function() { - var button = $( '' ) + +jQuery(document).ready( function ( $ ) { + var $button = $( '' ) .css( { - 'position': 'fixed', - 'bottom': 0, - 'right': 0, - 'width': '100%', - 'backgroundColor': '#333333', - 'opacity': 0.75, - 'color': '#DDDDDD', - 'padding': '0.5em', - 'border': 'none', - 'display': 'none' + position: 'fixed', + bottom: 0, + right: 0, + width: '100%', + backgroundColor: '#333', + opacity: 0.75, + color: '#DDDDDD', + padding: '0.7em', + border: 'none', + display: 'none' } ) - .click( function() { - if ( $(this).attr( 'enabled' ) == 'false' ) { + .click( function () { + if ( $(this).data( 'testDone' ) ) { $(this).slideUp( 'fast' ); return false; } - var messages = [ 'Running tests for wikiEditor API' ]; - var $target = $( textareaId ); - var $ui = $target.data( 'wikiEditor-context' ).$ui; - var passes = 0; - var tests = 0; - for ( var test in wikiEditorTests ) { - var pre = $ui.find( wikiEditorTests[test].test ).size() == - wikiEditorTests[test].pre; + + var test, pre, post, + messages = [ 'Running tests for wikiEditor API' ], + $target = $( textareaId ), + $ui = $target.data( 'wikiEditor-context' ).$ui, + passes = 0, + tests = 0; + + for ( test in wikiEditorTests ) { + pre = $ui.find( wikiEditorTests[test].test ).length === wikiEditorTests[test].pre; messages.push ( test + '-pre: ' + ( pre ? 'PASS' : 'FAIL' ) ); $target.wikiEditor( wikiEditorTests[test].call, wikiEditorTests[test].data ); - var post = $ui.find( wikiEditorTests[test].test ).size() == - wikiEditorTests[test].post; + post = $ui.find( wikiEditorTests[test].test ).length === wikiEditorTests[test].post; messages.push ( test + '-post: ' + ( post ? 'PASS' : 'FAIL' ) ); if ( pre && post ) { passes++; } tests++; } - if ( window.console !== undefined ) { + + if ( window.console ) { for ( var i = 0; i < messages.length; i++ ) { - console.log( messages[i] ); + window.console.log( messages[i] ); } } + $(this) .attr( 'title', messages.join( " | " ) ) .text( passes + ' / ' + tests + ' were successful' ) .css( 'backgroundColor', passes < tests ? 'red' : 'green' ) - .attr( 'enabled', 'false' ) + .data( 'testDone', 'true' ) .blur(); } ) .appendTo( $( 'body' ) ); - setTimeout( function() { button.slideDown( 'fast' ) }, 2000 ); + + setTimeout( function () { + $button.slideDown( 'fast' ); + }, 1500 ); + } ); diff --git a/modules/jquery.wikiEditor.dialogs.config.js b/modules/jquery.wikiEditor.dialogs.config.js index 0c24c368..54fcad79 100644 --- a/modules/jquery.wikiEditor.dialogs.config.js +++ b/modules/jquery.wikiEditor.dialogs.config.js @@ -1,1201 +1,1213 @@ /** * Configuration of Dialog module for wikiEditor */ -( function( $ ) { $.wikiEditor.modules.dialogs.config = { +( function ( $, mw ) { -replaceIcons: function( $textarea ) { - $textarea - .wikiEditor( 'removeFromToolbar', { 'section': 'main', 'group': 'insert', 'tool': 'xlink' } ) - .wikiEditor( 'removeFromToolbar', { 'section': 'main', 'group': 'insert', 'tool': 'ilink' } ) - .wikiEditor( 'removeFromToolbar', { 'section': 'main', 'group': 'insert', 'tool': 'reference' } ) - .wikiEditor( 'removeFromToolbar', { 'section': 'advanced', 'group': 'insert', 'tool': 'table' } ) - .wikiEditor( 'addToToolbar', { - 'section': 'main', - 'group': 'insert', - 'tools': { - 'link': { - 'labelMsg': 'wikieditor-toolbar-tool-link', - 'type': 'button', - 'icon': 'insert-link.png', - 'offset': [2, -1654], - 'action': { - 'type': 'dialog', - 'module': 'insert-link' - } - }, - 'reference': { - 'labelMsg': 'wikieditor-toolbar-tool-reference', - 'filters': [ 'body.ns-subject' ], - 'type': 'button', - 'icon': 'insert-reference.png', - 'offset': [2, -1798], - 'action': { - 'type': 'dialog', - 'module': 'insert-reference' +$.wikiEditor.modules.dialogs.config = { + + replaceIcons: function ( $textarea ) { + $textarea + .wikiEditor( 'removeFromToolbar', { section: 'main', group: 'insert', tool: 'xlink' } ) + .wikiEditor( 'removeFromToolbar', { section: 'main', group: 'insert', tool: 'ilink' } ) + .wikiEditor( 'removeFromToolbar', { section: 'main', group: 'insert', tool: 'reference' } ) + .wikiEditor( 'removeFromToolbar', { section: 'advanced', group: 'insert', tool: 'table' } ) + .wikiEditor( 'addToToolbar', { + section: 'main', + group: 'insert', + tools: { + 'link': { + labelMsg: 'wikieditor-toolbar-tool-link', + type: 'button', + icon: 'insert-link.png', + offset: [2, -1654], + action: { + type: 'dialog', + module: 'insert-link' + } + }, + 'reference': { + labelMsg: 'wikieditor-toolbar-tool-reference', + filters: [ 'body.ns-subject' ], + type: 'button', + icon: 'insert-reference.png', + offset: [2, -1798], + action: { + type: 'dialog', + module: 'insert-reference' + } } } - } - } ) - .wikiEditor( 'addToToolbar', { - 'section': 'advanced', - 'group': 'insert', - 'tools': { - 'table': { - 'labelMsg': 'wikieditor-toolbar-tool-table', - 'type': 'button', - 'icon': 'insert-table.png', - 'offset': [2, -1942], - 'action': { - 'type': 'dialog', - 'module': 'insert-table' + } ) + .wikiEditor( 'addToToolbar', { + section: 'advanced', + group: 'insert', + tools: { + 'table': { + labelMsg: 'wikieditor-toolbar-tool-table', + type: 'button', + icon: 'insert-table.png', + offset: [2, -1942], + action: { + type: 'dialog', + module: 'insert-table' + } } } - } - } ) - .wikiEditor( 'addToToolbar', { - 'section': 'advanced', - 'groups': { - 'search': { - 'tools': { - 'replace': { - 'labelMsg': 'wikieditor-toolbar-tool-replace', - 'type': 'button', - 'icon': 'search-replace.png', - 'offset': [-70, -214], - 'action': { - 'type': 'dialog', - 'module': 'search-and-replace' + } ) + .wikiEditor( 'addToToolbar', { + section: 'advanced', + groups: { + 'search': { + tools: { + 'replace': { + labelMsg: 'wikieditor-toolbar-tool-replace', + type: 'button', + icon: 'search-replace.png', + offset: [-70, -214], + action: { + type: 'dialog', + module: 'search-and-replace' + } } } } } - } - } ); -}, + } ); + }, -getDefaultConfig: function () { - return { 'dialogs': { - 'insert-link': { - titleMsg: 'wikieditor-toolbar-tool-link-title', - id: 'wikieditor-toolbar-link-dialog', - html: '\ -
\ -
\ - \ - \ - \ -
\ -
\ - \ - \ -
\ -
\ -
\ - \ - \ + getDefaultConfig: function () { + return { 'dialogs': { + 'insert-link': { + titleMsg: 'wikieditor-toolbar-tool-link-title', + id: 'wikieditor-toolbar-link-dialog', + html: '\ +
\ +
\ + \ + \ + \
\ -
\ - \ - \ +
\ + \ + \
\ -
\ -
', - init: function() { - function isExternalLink( s ) { - // The following things are considered to be external links: - // * Starts a URL protocol - // * Starts with www. - // All of these are potentially valid titles, and the latter two categories match about 6300 - // titles in enwiki's ns0. Out of 6.9M titles, that's 0.09% - if ( typeof arguments.callee.regex == 'undefined' ) { - // Cache the regex - arguments.callee.regex = - new RegExp( "^(" + mw.config.get( 'wgUrlProtocols' ) + "|www\\.)", 'i'); +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
\ +
', + + init: function () { + function isExternalLink( s ) { + // The following things are considered to be external links: + // * Starts a URL protocol + // * Starts with www. + // All of these are potentially valid titles, and the latter two categories match about 6300 + // titles in enwiki's ns0. Out of 6.9M titles, that's 0.09% + if ( typeof arguments.callee.regex === 'undefined' ) { + // Cache the regex + arguments.callee.regex = + new RegExp( "^(" + mw.config.get( 'wgUrlProtocols' ) + "|www\\.)", 'i'); + } + return s.match( arguments.callee.regex ); } - return s.match( arguments.callee.regex ); - } - // Updates the status indicator above the target link - function updateWidget( status ) { - $( '#wikieditor-toolbar-link-int-target-status' ).children().hide(); - $( '#wikieditor-toolbar-link-int-target' ).parent() - .removeClass( - 'status-invalid status-external status-notexists status-exists status-loading' - ); - if ( status ) { - $( '#wikieditor-toolbar-link-int-target-status-' + status ).show(); - $( '#wikieditor-toolbar-link-int-target' ).parent().addClass( 'status-' + status ); + + // Updates the status indicator above the target link + function updateWidget( status ) { + $( '#wikieditor-toolbar-link-int-target-status' ).children().hide(); + $( '#wikieditor-toolbar-link-int-target' ).parent() + .removeClass( + 'status-invalid status-external status-notexists status-exists status-loading' + ); + if ( status ) { + $( '#wikieditor-toolbar-link-int-target-status-' + status ).show(); + $( '#wikieditor-toolbar-link-int-target' ).parent().addClass( 'status-' + status ); + } + if ( status === 'invalid' ) { + $( '.ui-dialog:visible .ui-dialog-buttonpane button:first' ) + .attr( 'disabled', true ) + .addClass( 'disabled' ); + } else { + $( '.ui-dialog:visible .ui-dialog-buttonpane button:first' ) + .removeAttr('disabled') + .removeClass('disabled'); + } } - if ( status == 'invalid' ) { - $( '.ui-dialog:visible .ui-dialog-buttonpane button:first' ) - .attr( 'disabled', true ) - .addClass( 'disabled' ); - } else { - $( '.ui-dialog:visible .ui-dialog-buttonpane button:first' ) - .removeAttr('disabled') - .removeClass('disabled'); - } - } - // Updates the UI to show if the page title being inputed by the user exists or not - // accepts parameter internal for bypassing external link detection - function updateExistence( internal ) { - // ensure the internal parameter is a boolean - if ( internal != true ) internal = false; - // Abort previous request - var request = $( '#wikieditor-toolbar-link-int-target-status' ).data( 'request' ); - if ( request ) { - request.abort(); - } - var target = $( '#wikieditor-toolbar-link-int-target' ).val(); - var cache = $( '#wikieditor-toolbar-link-int-target-status' ).data( 'existencecache' ); - if ( cache[target] ) { - updateWidget( cache[target] ); - return; - } - if ( target.replace( /^\s+$/,'' ) == '' ) { - // Hide the widget when the textbox is empty - updateWidget( false ); - return; - } - // If the forced internal paremter was not true, check if the target is an external link - if ( !internal && isExternalLink( target ) ) { - updateWidget( 'external' ); - return; - } - if ( target.indexOf( '|' ) != -1 ) { - // Title contains | , which means it's invalid - // but confuses the API. Show invalid and bypass API - updateWidget( 'invalid' ); - return; - } - // Show loading spinner while waiting for the API to respond - updateWidget( 'loading' ); - // Call the API to check page status, saving the request object so it can be aborted if - // necessary - $( '#wikieditor-toolbar-link-int-target-status' ).data( - 'request', - $.ajax( { - url: mw.util.wikiScript( 'api' ), - dataType: 'json', - data: { - 'action': 'query', - 'indexpageids': '', - 'titles': target, - 'converttitles': '', - 'format': 'json' - }, - success: function( data ) { - var status; - if ( !data || typeof data.query == 'undefined' ) { - // This happens in some weird cases - status = false; - } else { - var page = data.query.pages[data.query.pageids[0]]; - status = 'exists'; - if ( typeof page.missing != 'undefined' ) - status = 'notexists'; - else if ( typeof page.invalid != 'undefined' ) - status = 'invalid'; - } - // Cache the status of the link target if the force internal parameter was not - // passed - if ( !internal ) cache[target] = status; - updateWidget( status ); - } - } ) - ); - } - $( '#wikieditor-toolbar-link-type-int, #wikieditor-toolbar-link-type-ext' ).click( function() { - if( $( '#wikieditor-toolbar-link-type-ext' ).is( ':checked' ) ) { + + // Updates the UI to show if the page title being inputed by the user exists or not + // accepts parameter internal for bypassing external link detection + function updateExistence( internal ) { + // ensure the internal parameter is a boolean + if ( internal !== true ) { + internal = false; + } // Abort previous request var request = $( '#wikieditor-toolbar-link-int-target-status' ).data( 'request' ); if ( request ) { request.abort(); } - updateWidget( 'external' ); + var target = $( '#wikieditor-toolbar-link-int-target' ).val(); + var cache = $( '#wikieditor-toolbar-link-int-target-status' ).data( 'existencecache' ); + if ( cache[target] ) { + updateWidget( cache[target] ); + return; + } + if ( target.replace( /^\s+$/,'' ) === '' ) { + // Hide the widget when the textbox is empty + updateWidget( false ); + return; + } + // If the forced internal paremter was not true, check if the target is an external link + if ( !internal && isExternalLink( target ) ) { + updateWidget( 'external' ); + return; + } + if ( target.indexOf( '|' ) !== -1 ) { + // Title contains | , which means it's invalid + // but confuses the API. Show invalid and bypass API + updateWidget( 'invalid' ); + return; + } + // Show loading spinner while waiting for the API to respond + updateWidget( 'loading' ); + // Call the API to check page status, saving the request object so it can be aborted if + // necessary + $( '#wikieditor-toolbar-link-int-target-status' ).data( + 'request', + $.ajax( { + url: mw.util.wikiScript( 'api' ), + dataType: 'json', + data: { + action: 'query', + indexpageids: '', + titles: target, + converttitles: '', + format: 'json' + }, + success: function ( data ) { + var status; + if ( !data || !data.query ) { + // This happens in some weird cases + status = false; + } else { + var page = data.query.pages[data.query.pageids[0]]; + status = 'exists'; + if ( page.missing ) { + status = 'notexists'; + } else if ( page.invalid ) { + status = 'invalid'; + } + } + // Cache the status of the link target if the force internal + // parameter was not passed + if ( !internal ) { + cache[target] = status; + } + updateWidget( status ); + } + } ) + ); } - if( $( '#wikieditor-toolbar-link-type-int' ).is( ':checked' ) ) - updateExistence( true ); - }); - // Set labels of tabs based on rel values - $(this).find( '[rel]' ).each( function() { - $(this).text( mediaWiki.msg( $(this).attr( 'rel' ) ) ); - }); - // Set tabindexes on form fields - $.wikiEditor.modules.dialogs.fn.setTabindexes( $(this).find( 'input' ).not( '[tabindex]' ) ); - // Setup the tooltips in the textboxes - $( '#wikieditor-toolbar-link-int-target' ) - .data( 'tooltip', mediaWiki.msg( 'wikieditor-toolbar-tool-link-int-target-tooltip' ) ); - $( '#wikieditor-toolbar-link-int-text' ) - .data( 'tooltip', mediaWiki.msg( 'wikieditor-toolbar-tool-link-int-text-tooltip' ) ); - $( '#wikieditor-toolbar-link-int-target, #wikieditor-toolbar-link-int-text' ) - .each( function() { - var tooltip = mediaWiki.msg( $( this ).attr( 'id' ) + '-tooltip' ); - if ( $( this ).val() == '' ) - $( this ) - .addClass( 'wikieditor-toolbar-dialog-hint' ) - .val( $( this ).data( 'tooltip' ) ) - .data( 'tooltip-mode', true ); - } ) - .focus( function() { - if( $( this ).val() == $( this ).data( 'tooltip' ) ) { - $( this ) - .val( '' ) - .removeClass( 'wikieditor-toolbar-dialog-hint' ) - .data( 'tooltip-mode', false ); + $( '#wikieditor-toolbar-link-type-int, #wikieditor-toolbar-link-type-ext' ).click( function () { + if ( $( '#wikieditor-toolbar-link-type-ext' ).is( ':checked' ) ) { + // Abort previous request + var request = $( '#wikieditor-toolbar-link-int-target-status' ).data( 'request' ); + if ( request ) { + request.abort(); + } + updateWidget( 'external' ); } - }) - .bind( 'change', function() { - if ( $( this ).val() != $( this ).data( 'tooltip' ) ) { - $( this ) - .removeClass( 'wikieditor-toolbar-dialog-hint' ) - .data( 'tooltip-mode', false ); - } - }) - .bind( 'blur', function() { - if ( $( this ).val() == '' ) { - $( this ) - .addClass( 'wikieditor-toolbar-dialog-hint' ) - .val( $( this ).data( 'tooltip' ) ) - .data( 'tooltip-mode', true ); + if ( $( '#wikieditor-toolbar-link-type-int' ).is( ':checked' ) ) + updateExistence( true ); + }); + // Set labels of tabs based on rel values + $(this).find( '[rel]' ).each( function () { + $(this).text( mw.msg( $(this).attr( 'rel' ) ) ); + }); + // Set tabindexes on form fields + $.wikiEditor.modules.dialogs.fn.setTabindexes( $(this).find( 'input' ).not( '[tabindex]' ) ); + // Setup the tooltips in the textboxes + $( '#wikieditor-toolbar-link-int-target' ) + .data( 'tooltip', mw.msg( 'wikieditor-toolbar-tool-link-int-target-tooltip' ) ); + $( '#wikieditor-toolbar-link-int-text' ) + .data( 'tooltip', mw.msg( 'wikieditor-toolbar-tool-link-int-text-tooltip' ) ); + $( '#wikieditor-toolbar-link-int-target, #wikieditor-toolbar-link-int-text' ) + .each( function () { + var tooltip = mw.msg( $( this ).attr( 'id' ) + '-tooltip' ); + if ( $( this ).val() === '' ) + $( this ) + .addClass( 'wikieditor-toolbar-dialog-hint' ) + .val( $( this ).data( 'tooltip' ) ) + .data( 'tooltip-mode', true ); + } ) + .focus( function () { + if ( $( this ).val() === $( this ).data( 'tooltip' ) ) { + $( this ) + .val( '' ) + .removeClass( 'wikieditor-toolbar-dialog-hint' ) + .data( 'tooltip-mode', false ); + } + }) + .bind( 'change', function () { + if ( $( this ).val() !== $( this ).data( 'tooltip' ) ) { + $( this ) + .removeClass( 'wikieditor-toolbar-dialog-hint' ) + .data( 'tooltip-mode', false ); + } + }) + .bind( 'blur', function () { + if ( $( this ).val() === '' ) { + $( this ) + .addClass( 'wikieditor-toolbar-dialog-hint' ) + .val( $( this ).data( 'tooltip' ) ) + .data( 'tooltip-mode', true ); + } + }); + + // Automatically copy the value of the internal link page title field to the link text field unless the + // user has changed the link text field - this is a convenience thing since most link texts are going to + // be the the same as the page title - Also change the internal/external radio button accordingly + $( '#wikieditor-toolbar-link-int-target' ).bind( 'change keydown paste cut', function () { + // $(this).val() is the old value, before the keypress - Defer this until $(this).val() has + // been updated + setTimeout( function () { + if ( isExternalLink( $( '#wikieditor-toolbar-link-int-target' ).val() ) ) { + $( '#wikieditor-toolbar-link-type-ext' ).prop( 'checked', true ); + updateWidget( 'external' ); + } else { + $( '#wikieditor-toolbar-link-type-int' ).prop( 'checked', true ); + updateExistence(); + } + if ( $( '#wikieditor-toolbar-link-int-text' ).data( 'untouched' ) ) + if ( $( '#wikieditor-toolbar-link-int-target' ).val() == + $( '#wikieditor-toolbar-link-int-target' ).data( 'tooltip' ) ) { + $( '#wikieditor-toolbar-link-int-text' ) + .addClass( 'wikieditor-toolbar-dialog-hint' ) + .val( $( '#wikieditor-toolbar-link-int-text' ).data( 'tooltip' ) ) + .change(); + } else { + $( '#wikieditor-toolbar-link-int-text' ) + .val( $( '#wikieditor-toolbar-link-int-target' ).val() ) + .change(); + } + }, 0 ); + }); + $( '#wikieditor-toolbar-link-int-text' ).bind( 'change keydown paste cut', function () { + var oldVal = $(this).val(); + var that = this; + setTimeout( function () { + if ( $(that).val() !== oldVal ) + $(that).data( 'untouched', false ); + }, 0 ); + }); + // Add images to the page existence widget, which will be shown mutually exclusively to communicate if + // the page exists, does not exist or the title is invalid (like if it contains a | character) + var existsMsg = mw.msg( 'wikieditor-toolbar-tool-link-int-target-status-exists' ); + var notexistsMsg = mw.msg( 'wikieditor-toolbar-tool-link-int-target-status-notexists' ); + var invalidMsg = mw.msg( 'wikieditor-toolbar-tool-link-int-target-status-invalid' ); + var externalMsg = mw.msg( 'wikieditor-toolbar-tool-link-int-target-status-external' ); + var loadingMsg = mw.msg( 'wikieditor-toolbar-tool-link-int-target-status-loading' ); + $( '#wikieditor-toolbar-link-int-target-status' ) + .append( $( '
' ) + .attr( 'id', 'wikieditor-toolbar-link-int-target-status-exists' ) + .append( existsMsg ) + ) + .append( $( '
' ) + .attr( 'id', 'wikieditor-toolbar-link-int-target-status-notexists' ) + .append( notexistsMsg ) + ) + .append( $( '
' ) + .attr( 'id', 'wikieditor-toolbar-link-int-target-status-invalid' ) + .append( invalidMsg ) + ) + .append( $( '
' ) + .attr( 'id', 'wikieditor-toolbar-link-int-target-status-external' ) + .append( externalMsg ) + ) + .append( $( '
' ) + .attr( 'id', 'wikieditor-toolbar-link-int-target-status-loading' ) + .append( $( '' ).attr( { + 'src': $.wikiEditor.imgPath + 'dialogs/' + 'loading-small.gif', + 'alt': loadingMsg, + 'title': loadingMsg + } ) ) + ) + .data( 'existencecache', {} ) + .children().hide(); + + $( '#wikieditor-toolbar-link-int-target' ) + .bind( 'keyup paste cut', function () { + // Cancel the running timer if applicable + if ( typeof $(this).data( 'timerID' ) !== 'undefined' ) { + clearTimeout( $(this).data( 'timerID' ) ); + } + // Delay fetch for a while + // FIXME: Make 120 configurable elsewhere + var timerID = setTimeout( updateExistence, 120 ); + $(this).data( 'timerID', timerID ); + } ) + .change( function () { + // Cancel the running timer if applicable + if ( typeof $(this).data( 'timerID' ) !== 'undefined' ) { + clearTimeout( $(this).data( 'timerID' ) ); + } + // Fetch right now + updateExistence(); + } ); + + // Title suggestions + $( '#wikieditor-toolbar-link-int-target' ).data( 'suggcache', {} ).suggestions( { + fetch: function ( query ) { + var that = this; + var title = $(this).val(); + + if ( isExternalLink( title ) || title.indexOf( '|' ) !== -1 || title === '') { + $(this).suggestions( 'suggestions', [] ); + return; + } + + var cache = $(this).data( 'suggcache' ); + if ( typeof cache[title] !== 'undefined' ) { + $(this).suggestions( 'suggestions', cache[title] ); + return; + } + + var request = $.ajax( { + url: mw.util.wikiScript( 'api' ), + data: { + action: 'opensearch', + search: title, + namespace: 0, + suggest: '', + format: 'json' + }, + dataType: 'json', + success: function ( data ) { + cache[title] = data[1]; + $(that).suggestions( 'suggestions', data[1] ); + } + }); + $(this).data( 'request', request ); + }, + cancel: function () { + var request = $(this).data( 'request' ); + if ( request ) + request.abort(); } }); - - // Automatically copy the value of the internal link page title field to the link text field unless the - // user has changed the link text field - this is a convenience thing since most link texts are going to - // be the the same as the page title - Also change the internal/external radio button accordingly - $( '#wikieditor-toolbar-link-int-target' ).bind( 'change keydown paste cut', function() { - // $(this).val() is the old value, before the keypress - Defer this until $(this).val() has - // been updated - setTimeout( function() { - if ( isExternalLink( $( '#wikieditor-toolbar-link-int-target' ).val() ) ) { - $( '#wikieditor-toolbar-link-type-ext' ).attr( 'checked', 'checked' ); - updateWidget( 'external' ); - } else { - $( '#wikieditor-toolbar-link-type-int' ).attr( 'checked', 'checked' ); - updateExistence(); - } - if ( $( '#wikieditor-toolbar-link-int-text' ).data( 'untouched' ) ) - if ( $( '#wikieditor-toolbar-link-int-target' ).val() == - $( '#wikieditor-toolbar-link-int-target' ).data( 'tooltip' ) ) { - $( '#wikieditor-toolbar-link-int-text' ) - .addClass( 'wikieditor-toolbar-dialog-hint' ) - .val( $( '#wikieditor-toolbar-link-int-text' ).data( 'tooltip' ) ) - .change(); - } else { - $( '#wikieditor-toolbar-link-int-text' ) - .val( $( '#wikieditor-toolbar-link-int-target' ).val() ) - .change(); - } - }, 0 ); - }); - $( '#wikieditor-toolbar-link-int-text' ).bind( 'change keydown paste cut', function() { - var oldVal = $(this).val(); - var that = this; - setTimeout( function() { - if ( $(that).val() != oldVal ) - $(that).data( 'untouched', false ); - }, 0 ); - }); - // Add images to the page existence widget, which will be shown mutually exclusively to communicate if - // the page exists, does not exist or the title is invalid (like if it contains a | character) - var existsMsg = mediaWiki.msg( 'wikieditor-toolbar-tool-link-int-target-status-exists' ); - var notexistsMsg = mediaWiki.msg( 'wikieditor-toolbar-tool-link-int-target-status-notexists' ); - var invalidMsg = mediaWiki.msg( 'wikieditor-toolbar-tool-link-int-target-status-invalid' ); - var externalMsg = mediaWiki.msg( 'wikieditor-toolbar-tool-link-int-target-status-external' ); - var loadingMsg = mediaWiki.msg( 'wikieditor-toolbar-tool-link-int-target-status-loading' ); - $( '#wikieditor-toolbar-link-int-target-status' ) - .append( $( '
' ) - .attr( 'id', 'wikieditor-toolbar-link-int-target-status-exists' ) - .append( existsMsg ) - ) - .append( $( '
' ) - .attr( 'id', 'wikieditor-toolbar-link-int-target-status-notexists' ) - .append( notexistsMsg ) - ) - .append( $( '
' ) - .attr( 'id', 'wikieditor-toolbar-link-int-target-status-invalid' ) - .append( invalidMsg ) - ) - .append( $( '
' ) - .attr( 'id', 'wikieditor-toolbar-link-int-target-status-external' ) - .append( externalMsg ) - ) - .append( $( '
' ) - .attr( 'id', 'wikieditor-toolbar-link-int-target-status-loading' ) - .append( $( '' ).attr( { - 'src': $.wikiEditor.imgPath + 'dialogs/' + 'loading-small.gif', - 'alt': loadingMsg, - 'title': loadingMsg - } ) ) - ) - .data( 'existencecache', {} ) - .children().hide(); - - $( '#wikieditor-toolbar-link-int-target' ) - .bind( 'keyup paste cut', function() { - // Cancel the running timer if applicable - if ( typeof $(this).data( 'timerID' ) != 'undefined' ) { - clearTimeout( $(this).data( 'timerID' ) ); - } - // Delay fetch for a while - // FIXME: Make 120 configurable elsewhere - var timerID = setTimeout( updateExistence, 120 ); - $(this).data( 'timerID', timerID ); - } ) - .change( function() { - // Cancel the running timer if applicable - if ( typeof $(this).data( 'timerID' ) != 'undefined' ) { - clearTimeout( $(this).data( 'timerID' ) ); - } - // Fetch right now - updateExistence(); - } ); - - // Title suggestions - $( '#wikieditor-toolbar-link-int-target' ).data( 'suggcache', {} ).suggestions( { - fetch: function( query ) { - var that = this; - var title = $(this).val(); - - if ( isExternalLink( title ) || title.indexOf( '|' ) != -1 || title == '') { - $(this).suggestions( 'suggestions', [] ); - return; - } - - var cache = $(this).data( 'suggcache' ); - if ( typeof cache[title] != 'undefined' ) { - $(this).suggestions( 'suggestions', cache[title] ); - return; - } - - var request = $.ajax( { - url: mw.util.wikiScript( 'api' ), - data: { - 'action': 'opensearch', - 'search': title, - 'namespace': 0, - 'suggest': '', - 'format': 'json' - }, - dataType: 'json', - success: function( data ) { - cache[title] = data[1]; - $(that).suggestions( 'suggestions', data[1] ); - } - }); - $(this).data( 'request', request ); - }, - cancel: function() { - var request = $(this).data( 'request' ); - if ( request ) - request.abort(); - } - }); - }, - dialog: { - width: 500, - dialogClass: 'wikiEditor-toolbar-dialog', - buttons: { - 'wikieditor-toolbar-tool-link-insert': function() { - function escapeInternalText( s ) { - // FIXME: Should this escape [[ too? Seems to work without that - return s.replace( /(]{2,})/g, '$1' ); - } - function escapeExternalTarget( s ) { - return s.replace( / /g, '%20' ) - .replace( /\[/g, '%5B' ) - .replace( /]/g, '%5D' ); - } - function escapeExternalText( s ) { - // FIXME: Should this escape [ too? Seems to work without that - return s.replace( /(]+)/g, '$1' ); - } - var insertText = ''; - var whitespace = $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace' ); - var target = $( '#wikieditor-toolbar-link-int-target' ).val(); - var text = $( '#wikieditor-toolbar-link-int-text' ).val(); - // check if the tooltips were passed as target or text - if ( $( '#wikieditor-toolbar-link-int-target' ).data( 'tooltip-mode' ) ) - target = ""; - if ( $( '#wikieditor-toolbar-link-int-text' ).data( 'tooltip-mode' ) ) - text = ""; - if ( target == '' ) { - alert( mediaWiki.msg( 'wikieditor-toolbar-tool-link-empty' ) ); - return; - } - if ( $.trim( text ) == '' ) { - // [[Foo| ]] creates an invisible link - // Instead, generate [[Foo|]] - text = ''; - } - if ( $( '#wikieditor-toolbar-link-type-int' ).is( ':checked' ) ) { - // FIXME: Exactly how fragile is this? - if ( $( '#wikieditor-toolbar-link-int-target-status-invalid' ).is( ':visible' ) ) { - // Refuse to add links to invalid titles - alert( mediaWiki.msg( 'wikieditor-toolbar-tool-link-int-invalid' ) ); - return; - } - - if ( target == text || !text.length ) - insertText = '[[' + target + ']]'; - else - insertText = '[[' + target + '|' + escapeInternalText( text ) + ']]'; - } else { - // Prepend http:// if there is no protocol - if ( !target.match( /^[a-z]+:\/\/./ ) ) - target = 'http://' + target; - - // Detect if this is really an internal link in disguise - var match = target.match( $(this).data( 'articlePathRegex' ) ); - if ( match && !$(this).data( 'ignoreLooksInternal' ) ) { - var buttons = { }; - var that = this; - buttons[ mediaWiki.msg( 'wikieditor-toolbar-tool-link-lookslikeinternal-int' ) ] = - function() { - $( '#wikieditor-toolbar-link-int-target' ).val( match[1] ).change(); - $(this).dialog( 'close' ); - }; - buttons[ mediaWiki.msg( 'wikieditor-toolbar-tool-link-lookslikeinternal-ext' ) ] = - function() { - $(that).data( 'ignoreLooksInternal', true ); - $(that).closest( '.ui-dialog' ).find( 'button:first' ).click(); - $(that).data( 'ignoreLooksInternal', false ); - $(this).dialog( 'close' ); - }; - $.wikiEditor.modules.dialogs.quickDialog( - mediaWiki.msg( 'wikieditor-toolbar-tool-link-lookslikeinternal', match[1] ), - { buttons: buttons } - ); - return; - } - - var escTarget = escapeExternalTarget( target ); - var escText = escapeExternalText( text ); - - if ( escTarget == escText ) - insertText = escTarget; - else if ( text == '' ) - insertText = '[' + escTarget + ']'; - else - insertText = '[' + escTarget + ' ' + escText + ']'; - } - // Preserve whitespace in selection when replacing - if ( whitespace ) insertText = whitespace[0] + insertText + whitespace[1]; - $(this).dialog( 'close' ); - $.wikiEditor.modules.toolbar.fn.doAction( $(this).data( 'context' ), { - type: 'replace', - options: { - pre: insertText - } - }, $(this) ); - - // Blank form - $( '#wikieditor-toolbar-link-int-target, #wikieditor-toolbar-link-int-text' ).val( '' ); - $( '#wikieditor-toolbar-link-type-int, #wikieditor-toolbar-link-type-ext' ) - .attr( 'checked', '' ); - }, - 'wikieditor-toolbar-tool-link-cancel': function() { - // Clear any saved selection state - var context = $(this).data( 'context' ); - context.fn.restoreCursorAndScrollTop(); - $(this).dialog( 'close' ); - } }, - open: function() { - // Obtain the server name without the protocol. wgServer may be protocol-relative - var serverName = mw.config.get( 'wgServer' ).replace( /^(https?:)?\/\//, '' ); - // Cache the articlepath regex - $(this).data( 'articlePathRegex', new RegExp( - '^https?://' + $.escapeRE( serverName + mw.config.get( 'wgArticlePath' ) ) - .replace( /\\\$1/g, '(.*)' ) + '$' - ) ); - // Pre-fill the text fields based on the current selection - var context = $(this).data( 'context' ); - // Restore and immediately save selection state, needed for inserting stuff later - context.fn.restoreCursorAndScrollTop(); - context.fn.saveCursorAndScrollTop(); - var selection = context.$textarea.textSelection( 'getSelection' ); - $( '#wikieditor-toolbar-link-int-target' ).focus(); - // Trigger the change event, so the link status indicator is up to date - $( '#wikieditor-toolbar-link-int-target' ).change(); - $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace', [ '', '' ] ); - if ( selection != '' ) { - var target, text, type; - var matches; - if ( ( matches = selection.match( /^(\s*)\[\[([^\]\|]+)(\|([^\]\|]*))?\]\](\s*)$/ ) ) ) { - // [[foo|bar]] or [[foo]] - target = matches[2]; - text = ( matches[4] ? matches[4] : matches[2] ); - type = 'int'; - // Preserve whitespace when replacing - $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace', [ matches[1], matches[5] ] ); - } else if ( ( matches = selection.match( /^(\s*)\[([^\] ]+)( ([^\]]+))?\](\s*)$/ ) ) ) { - // [http://www.example.com foo] or [http://www.example.com] - target = matches[2]; - text = ( matches[4] ? matches[4] : '' ); - type = 'ext'; - // Preserve whitespace when replacing - $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace', [ matches[1], matches[5] ] ); - } else { - // Trim any leading and trailing whitespace from the selection, - // but preserve it when replacing - target = text = $.trim( selection ); - if ( target.length < selection.length ) { - $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace', [ - selection.substr( 0, selection.indexOf( target.charAt( 0 ) ) ), - selection.substr( - selection.lastIndexOf( target.charAt( target.length - 1 ) ) + 1 - ) ] - ); + dialog: { + width: 500, + dialogClass: 'wikiEditor-toolbar-dialog', + buttons: { + 'wikieditor-toolbar-tool-link-insert': function () { + function escapeInternalText( s ) { + return s.replace( /(\]{2,})/g, '$1' ); } - } - - // Change the value by calling val() doesn't trigger the change event, so let's do that - // ourselves - if ( typeof text != 'undefined' ) - $( '#wikieditor-toolbar-link-int-text' ).val( text ).change(); - if ( typeof target != 'undefined' ) - $( '#wikieditor-toolbar-link-int-target' ).val( target ).change(); - if ( typeof type != 'undefined' ) - $( '#wikieditor-toolbar-link-' + type ).attr( 'checked', 'checked' ); - } - $( '#wikieditor-toolbar-link-int-text' ).data( 'untouched', - $( '#wikieditor-toolbar-link-int-text' ).val() == - $( '#wikieditor-toolbar-link-int-target' ).val() || - $( '#wikieditor-toolbar-link-int-text' ).hasClass( 'wikieditor-toolbar-dialog-hint' ) - ); - $( '#wikieditor-toolbar-link-int-target' ).suggestions(); - - //don't overwrite user's text - if( selection != '' ){ - $( '#wikieditor-toolbar-link-int-text' ).data( 'untouched', false ); - } - - $( '#wikieditor-toolbar-link-int-text, #wikiedit-toolbar-link-int-target' ) - .each( function() { - if ( $(this).val() == '' ) - $(this).parent().find( 'label' ).show(); - }); - - if ( !( $(this).data( 'dialogkeypressset' ) ) ) { - $(this).data( 'dialogkeypressset', true ); - // Execute the action associated with the first button - // when the user presses Enter - $(this).closest( '.ui-dialog' ).keypress( function( e ) { - if ( ( e.keyCode || e.which ) == 13 ) { - var button = $(this).data( 'dialogaction' ) || $(this).find( 'button:first' ); - button.click(); - e.preventDefault(); + function escapeExternalTarget( s ) { + return s.replace( / /g, '%20' ) + .replace( /\[/g, '%5B' ) + .replace( /\]/g, '%5D' ); } - }); + function escapeExternalText( s ) { + return s.replace( /(\]+)/g, '$1' ); + } + var insertText = ''; + var whitespace = $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace' ); + var target = $( '#wikieditor-toolbar-link-int-target' ).val(); + var text = $( '#wikieditor-toolbar-link-int-text' ).val(); + // check if the tooltips were passed as target or text + if ( $( '#wikieditor-toolbar-link-int-target' ).data( 'tooltip-mode' ) ) + target = ""; + if ( $( '#wikieditor-toolbar-link-int-text' ).data( 'tooltip-mode' ) ) + text = ""; + if ( target === '' ) { + alert( mw.msg( 'wikieditor-toolbar-tool-link-empty' ) ); + return; + } + if ( $.trim( text ) === '' ) { + // [[Foo| ]] creates an invisible link + // Instead, generate [[Foo|]] + text = ''; + } + if ( $( '#wikieditor-toolbar-link-type-int' ).is( ':checked' ) ) { + // FIXME: Exactly how fragile is this? + if ( $( '#wikieditor-toolbar-link-int-target-status-invalid' ).is( ':visible' ) ) { + // Refuse to add links to invalid titles + alert( mw.msg( 'wikieditor-toolbar-tool-link-int-invalid' ) ); + return; + } - // Make tabbing to a button and pressing - // Enter do what people expect - $(this).closest( '.ui-dialog' ).find( 'button' ).focus( function() { - $(this).closest( '.ui-dialog' ).data( 'dialogaction', this ); - }); - } - } - } - }, - 'insert-reference': { - titleMsg: 'wikieditor-toolbar-tool-reference-title', - id: 'wikieditor-toolbar-reference-dialog', - html: '\ -
\ -
\ -
\ - \ - \ -
\ -
\ -
', - init: function() { - // Insert translated strings into labels - $( this ).find( '[rel]' ).each( function() { - $( this ).text( mediaWiki.msg( $( this ).attr( 'rel' ) ) ); - } ); + if ( target === text || !text.length ) + insertText = '[[' + target + ']]'; + else + insertText = '[[' + target + '|' + escapeInternalText( text ) + ']]'; + } else { + // Prepend http:// if there is no protocol + if ( !target.match( /^[a-z]+:\/\/./ ) ) + target = 'http://' + target; - }, - dialog: { - dialogClass: 'wikiEditor-toolbar-dialog', - width: 590, - buttons: { - 'wikieditor-toolbar-tool-reference-insert': function() { - var insertText = $( '#wikieditor-toolbar-reference-text' ).val(); - var whitespace = $( '#wikieditor-toolbar-reference-dialog' ).data( 'whitespace' ); - var attributes = $( '#wikieditor-toolbar-reference-dialog' ).data( 'attributes' ); - // Close the dialog - $( this ).dialog( 'close' ); - $.wikiEditor.modules.toolbar.fn.doAction( - $( this ).data( 'context' ), - { + // Detect if this is really an internal link in disguise + var match = target.match( $(this).data( 'articlePathRegex' ) ); + if ( match && !$(this).data( 'ignoreLooksInternal' ) ) { + var buttons = { }; + var that = this; + buttons[ mw.msg( 'wikieditor-toolbar-tool-link-lookslikeinternal-int' ) ] = + function () { + $( '#wikieditor-toolbar-link-int-target' ).val( match[1] ).change(); + $(this).dialog( 'close' ); + }; + buttons[ mw.msg( 'wikieditor-toolbar-tool-link-lookslikeinternal-ext' ) ] = + function () { + $(that).data( 'ignoreLooksInternal', true ); + $(that).closest( '.ui-dialog' ).find( 'button:first' ).click(); + $(that).data( 'ignoreLooksInternal', false ); + $(this).dialog( 'close' ); + }; + $.wikiEditor.modules.dialogs.quickDialog( + mw.msg( 'wikieditor-toolbar-tool-link-lookslikeinternal', match[1] ), + { buttons: buttons } + ); + return; + } + + var escTarget = escapeExternalTarget( target ); + var escText = escapeExternalText( text ); + + if ( escTarget === escText ) + insertText = escTarget; + else if ( text === '' ) + insertText = '[' + escTarget + ']'; + else + insertText = '[' + escTarget + ' ' + escText + ']'; + } + // Preserve whitespace in selection when replacing + if ( whitespace ) { + insertText = whitespace[0] + insertText + whitespace[1]; + } + $(this).dialog( 'close' ); + $.wikiEditor.modules.toolbar.fn.doAction( $(this).data( 'context' ), { type: 'replace', options: { - pre: whitespace[0] + '', - peri: insertText, - post: '' + whitespace[1] + pre: insertText } - }, - $( this ) - ); - // Restore form state - $( '#wikieditor-toolbar-reference-text' ).val( "" ); - }, - 'wikieditor-toolbar-tool-reference-cancel': function() { - // Clear any saved selection state - var context = $( this ).data( 'context' ); - context.fn.restoreCursorAndScrollTop(); - $( this ).dialog( 'close' ); - } - }, - open: function() { - // Pre-fill the text fields based on the current selection - var context = $(this).data( 'context' ); - // Restore and immediately save selection state, needed for inserting stuff later - context.fn.restoreCursorAndScrollTop(); - context.fn.saveCursorAndScrollTop(); - var selection = context.$textarea.textSelection( 'getSelection' ); - // set focus - $( '#wikieditor-toolbar-reference-text' ).focus(); - $( '#wikieditor-toolbar-reference-dialog' ) - .data( 'whitespace', [ '', '' ] ) - .data( 'attributes', '' ); - if ( selection != '' ) { - var matches, text; - if ( ( matches = selection.match( /^(\s*)]*)>([^\<]*)<\/ref\>(\s*)$/ ) ) ) { - text = matches[3]; - // Preserve whitespace when replacing - $( '#wikieditor-toolbar-reference-dialog' ) - .data( 'whitespace', [ matches[1], matches[4] ] ); - $( '#wikieditor-toolbar-reference-dialog' ).data( 'attributes', matches[2] ); - } else { - text = selection; + }, $(this) ); + + // Blank form + $( '#wikieditor-toolbar-link-int-target, #wikieditor-toolbar-link-int-text' ).val( '' ); + $( '#wikieditor-toolbar-link-type-int, #wikieditor-toolbar-link-type-ext' ) + .attr( 'checked', '' ); + }, + 'wikieditor-toolbar-tool-link-cancel': function () { + // Clear any saved selection state + var context = $(this).data( 'context' ); + context.fn.restoreCursorAndScrollTop(); + $(this).dialog( 'close' ); } - $( '#wikieditor-toolbar-reference-text' ).val( text ); - } - if ( !( $( this ).data( 'dialogkeypressset' ) ) ) { - $( this ).data( 'dialogkeypressset', true ); - // Execute the action associated with the first button - // when the user presses Enter - $( this ).closest( '.ui-dialog' ).keypress( function( e ) { - if ( ( e.keyCode || e.which ) == 13 ) { - var button = $( this ).data( 'dialogaction' ) || $( this ).find( 'button:first' ); - button.click(); - e.preventDefault(); + }, + open: function () { + var target, text, type, matches; + + // Obtain the server name without the protocol. wgServer may be protocol-relative + var serverName = mw.config.get( 'wgServer' ).replace( /^(https?:)?\/\//, '' ); + // Cache the articlepath regex + $(this).data( 'articlePathRegex', new RegExp( + '^https?://' + $.escapeRE( serverName + mw.config.get( 'wgArticlePath' ) ) + .replace( /\\\$1/g, '(.*)' ) + '$' + ) ); + // Pre-fill the text fields based on the current selection + var context = $(this).data( 'context' ); + // Restore and immediately save selection state, needed for inserting stuff later + context.fn.restoreCursorAndScrollTop(); + context.fn.saveCursorAndScrollTop(); + var selection = context.$textarea.textSelection( 'getSelection' ); + $( '#wikieditor-toolbar-link-int-target' ).focus(); + // Trigger the change event, so the link status indicator is up to date + $( '#wikieditor-toolbar-link-int-target' ).change(); + $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace', [ '', '' ] ); + if ( selection !== '' ) { + if ( ( matches = selection.match( /^(\s*)\[\[([^\]\|]+)(\|([^\]\|]*))?\]\](\s*)$/ ) ) ) { + // [[foo|bar]] or [[foo]] + target = matches[2]; + text = ( matches[4] ? matches[4] : matches[2] ); + type = 'int'; + // Preserve whitespace when replacing + $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace', [ matches[1], matches[5] ] ); + } else if ( ( matches = selection.match( /^(\s*)\[([^\] ]+)( ([^\]]+))?\](\s*)$/ ) ) ) { + // [http://www.example.com foo] or [http://www.example.com] + target = matches[2]; + text = ( matches[4] || '' ); + type = 'ext'; + // Preserve whitespace when replacing + $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace', [ matches[1], matches[5] ] ); + } else { + // Trim any leading and trailing whitespace from the selection, + // but preserve it when replacing + target = text = $.trim( selection ); + if ( target.length < selection.length ) { + $( '#wikieditor-toolbar-link-dialog' ).data( 'whitespace', [ + selection.substr( 0, selection.indexOf( target.charAt( 0 ) ) ), + selection.substr( + selection.lastIndexOf( target.charAt( target.length - 1 ) ) + 1 + ) ] + ); + } } - } ); - // Make tabbing to a button and pressing - // Enter do what people expect - $( this ).closest( '.ui-dialog' ).find( 'button' ).focus( function() { - $( this ).closest( '.ui-dialog' ).data( 'dialogaction', this ); - } ); + + // Change the value by calling val() doesn't trigger the change event, so let's do that + // ourselves + if ( typeof text !== 'undefined' ) + $( '#wikieditor-toolbar-link-int-text' ).val( text ).change(); + if ( typeof target !== 'undefined' ) + $( '#wikieditor-toolbar-link-int-target' ).val( target ).change(); + if ( typeof type !== 'undefined' ) + $( '#wikieditor-toolbar-link-' + type ).prop( 'checked', true ); + } + $( '#wikieditor-toolbar-link-int-text' ).data( 'untouched', + $( '#wikieditor-toolbar-link-int-text' ).val() === + $( '#wikieditor-toolbar-link-int-target' ).val() || + $( '#wikieditor-toolbar-link-int-text' ).hasClass( 'wikieditor-toolbar-dialog-hint' ) + ); + $( '#wikieditor-toolbar-link-int-target' ).suggestions(); + + // don't overwrite user's text + if ( selection !== '' ){ + $( '#wikieditor-toolbar-link-int-text' ).data( 'untouched', false ); + } + + $( '#wikieditor-toolbar-link-int-text, #wikiedit-toolbar-link-int-target' ) + .each( function () { + if ( $(this).val() === '' ) + $(this).parent().find( 'label' ).show(); + }); + + if ( !$(this).data( 'dialogkeypressset' ) ) { + $(this).data( 'dialogkeypressset', true ); + // Execute the action associated with the first button + // when the user presses Enter + $(this).closest( '.ui-dialog' ).keypress( function ( e ) { + if ( ( e.keyCode || e.which ) == 13 ) { + var button = $(this).data( 'dialogaction' ) || $(this).find( 'button:first' ); + button.click(); + e.preventDefault(); + } + }); + + // Make tabbing to a button and pressing + // Enter do what people expect + $(this).closest( '.ui-dialog' ).find( 'button' ).focus( function () { + $(this).closest( '.ui-dialog' ).data( 'dialogaction', this ); + }); + } } } - } - }, - 'insert-table': { - titleMsg: 'wikieditor-toolbar-tool-table-title', - id: 'wikieditor-toolbar-table-dialog', - // FIXME: Localize 'x'? - html: '\ + }, + 'insert-reference': { + titleMsg: 'wikieditor-toolbar-tool-reference-title', + id: 'wikieditor-toolbar-reference-dialog', + html: '\
\
\
\ - \ - \ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ -
\ -
\ -
\ - \ -
\ -
\ -
\ - \ -
\ + \ + \
\
\ -
\ - \ -
\ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -
\ -
\ -
', - init: function() { - $(this).find( '[rel]' ).each( function() { - $(this).text( mediaWiki.msg( $(this).attr( 'rel' ) ) ); - }); - // Set tabindexes on form fields - $.wikiEditor.modules.dialogs.fn.setTabindexes( $(this).find( 'input' ).not( '[tabindex]' ) ); +
', + init: function () { + // Insert translated strings into labels + $( this ).find( '[rel]' ).each( function () { + $( this ).text( mw.msg( $( this ).attr( 'rel' ) ) ); + } ); - $( '#wikieditor-toolbar-table-dimensions-rows' ).val( 3 ); - $( '#wikieditor-toolbar-table-dimensions-columns' ).val( 3 ); - $( '#wikieditor-toolbar-table-wikitable' ).click( function() { - $( '.wikieditor-toolbar-table-preview' ).toggleClass( 'wikitable' ); - }); - - // Hack for sortable preview: dynamically adding - // sortable class doesn't work, so we use a clone - $( '#wikieditor-toolbar-table-preview' ) - .clone() - .attr( 'id', 'wikieditor-toolbar-table-preview2' ) - .addClass( 'sortable' ) - .insertAfter( $( '#wikieditor-toolbar-table-preview' ) ) - .hide(); - - mw.loader.using( 'jquery.tablesorter', function() { - $( '#wikieditor-toolbar-table-preview2' ).tablesorter(); - }); - - $( '#wikieditor-toolbar-table-sortable' ).click( function() { - // Swap the currently shown one clone with the other one - $( '#wikieditor-toolbar-table-preview' ) - .hide() - .attr( 'id', 'wikieditor-toolbar-table-preview3' ); - $( '#wikieditor-toolbar-table-preview2' ) - .attr( 'id', 'wikieditor-toolbar-table-preview' ) - .show(); - $( '#wikieditor-toolbar-table-preview3' ).attr( 'id', 'wikieditor-toolbar-table-preview2' ); - }); - - $( '#wikieditor-toolbar-table-dimensions-header' ).click( function() { - // Instead of show/hiding, switch the HTML around - // We do this because the sortable tables script styles the first row, - // visible or not - var headerHTML = $( '.wikieditor-toolbar-table-preview-header' ).html(); - var hiddenHTML = $( '.wikieditor-toolbar-table-preview-hidden' ).html(); - $( '.wikieditor-toolbar-table-preview-header' ).html( hiddenHTML ); - $( '.wikieditor-toolbar-table-preview-hidden' ).html( headerHTML ); - if ( typeof jQuery.fn.tablesorter == 'function' ) { - $( '#wikieditor-toolbar-table-preview, #wikieditor-toolbar-table-preview2' ) - .filter( '.sortable' ) - .tablesorter(); - } - }); - }, - dialog: { - resizable: false, - dialogClass: 'wikiEditor-toolbar-dialog', - width: 590, - buttons: { - 'wikieditor-toolbar-tool-table-insert': function() { - var rowsVal = $( '#wikieditor-toolbar-table-dimensions-rows' ).val(); - var colsVal = $( '#wikieditor-toolbar-table-dimensions-columns' ).val(); - var rows = parseInt( rowsVal, 10 ); - var cols = parseInt( colsVal, 10 ); - var header = $( '#wikieditor-toolbar-table-dimensions-header' ).is( ':checked' ) ? 1 : 0; - if ( isNaN( rows ) || isNaN( cols ) || rows != rowsVal || cols != colsVal ) { - alert( mediaWiki.msg( 'wikieditor-toolbar-tool-table-invalidnumber' ) ); - return; + }, + dialog: { + dialogClass: 'wikiEditor-toolbar-dialog', + width: 590, + buttons: { + 'wikieditor-toolbar-tool-reference-insert': function () { + var insertText = $( '#wikieditor-toolbar-reference-text' ).val(); + var whitespace = $( '#wikieditor-toolbar-reference-dialog' ).data( 'whitespace' ); + var attributes = $( '#wikieditor-toolbar-reference-dialog' ).data( 'attributes' ); + // Close the dialog + $( this ).dialog( 'close' ); + $.wikiEditor.modules.toolbar.fn.doAction( + $( this ).data( 'context' ), + { + type: 'replace', + options: { + pre: whitespace[0] + '', + peri: insertText, + post: '' + whitespace[1] + } + }, + $( this ) + ); + // Restore form state + $( '#wikieditor-toolbar-reference-text' ).val( '' ); + }, + 'wikieditor-toolbar-tool-reference-cancel': function () { + // Clear any saved selection state + var context = $( this ).data( 'context' ); + context.fn.restoreCursorAndScrollTop(); + $( this ).dialog( 'close' ); } - if ( rows + header == 0 || cols == 0 ) { - alert( mediaWiki.msg( 'wikieditor-toolbar-tool-table-zero' ) ); - return; - } - if ( rows * cols > 1000 ) { - alert( mediaWiki.msg( 'wikieditor-toolbar-tool-table-toomany', 1000 ) ); - return; - } - var headerText = mediaWiki.msg( 'wikieditor-toolbar-tool-table-example-header' ); - var normalText = mediaWiki.msg( 'wikieditor-toolbar-tool-table-example' ); - var table = ""; - for ( var r = 0; r < rows + header; r++ ) { - table += "|-\n"; - for ( var c = 0; c < cols; c++ ) { - var isHeader = ( header && r == 0 ); - var delim = isHeader ? '!' : '|'; - if ( c > 0 ) { - delim += delim; - } - table += delim + ' ' + ( isHeader ? headerText : normalText ) + ' '; - } - // Replace trailing space by newline - // table[table.length - 1] is read-only - table = table.substr( 0, table.length - 1 ) + "\n"; - } - var classes = []; - if ( $( '#wikieditor-toolbar-table-wikitable' ).is( ':checked' ) ) - classes.push( 'wikitable' ); - if ( $( '#wikieditor-toolbar-table-sortable' ).is( ':checked' ) ) - classes.push( 'sortable' ); - var classStr = classes.length > 0 ? ' class="' + classes.join( ' ' ) + '"' : ''; - $(this).dialog( 'close' ); - $.wikiEditor.modules.toolbar.fn.doAction( - $(this).data( 'context' ), - { - type: 'replace', - options: { - pre: '{|' + classStr + "\n", - peri: table, - post: '|}', - ownline: true - } - }, - $(this) - ); - - // Restore form state - $( '#wikieditor-toolbar-table-dimensions-rows' ).val( 3 ); - $( '#wikieditor-toolbar-table-dimensions-columns' ).val( 3 ); - // Simulate clicks instead of setting values, so the according - // actions are performed - if ( !$( '#wikieditor-toolbar-table-dimensions-header' ).is( ':checked' ) ) - $( '#wikieditor-toolbar-table-dimensions-header' ).click(); - if ( !$( '#wikieditor-toolbar-table-wikitable' ).is( ':checked' ) ) - $( '#wikieditor-toolbar-table-wikitable' ).click(); - if ( $( '#wikieditor-toolbar-table-sortable' ).is( ':checked' ) ) - $( '#wikieditor-toolbar-table-sortable' ).click(); }, - 'wikieditor-toolbar-tool-table-cancel': function() { - $(this).dialog( 'close' ); - } - }, - open: function() { - $( '#wikieditor-toolbar-table-dimensions-rows' ).focus(); - if ( !( $(this).data( 'dialogkeypressset' ) ) ) { - $(this).data( 'dialogkeypressset', true ); - // Execute the action associated with the first button - // when the user presses Enter - $(this).closest( '.ui-dialog' ).keypress( function( e ) { - if ( ( e.keyCode || e.which ) == 13 ) { - var button = $(this).data( 'dialogaction' ) || $(this).find( 'button:first' ); - button.click(); - e.preventDefault(); + open: function () { + // Pre-fill the text fields based on the current selection + var context = $(this).data( 'context' ); + // Restore and immediately save selection state, needed for inserting stuff later + context.fn.restoreCursorAndScrollTop(); + context.fn.saveCursorAndScrollTop(); + var selection = context.$textarea.textSelection( 'getSelection' ); + // set focus + $( '#wikieditor-toolbar-reference-text' ).focus(); + $( '#wikieditor-toolbar-reference-dialog' ) + .data( 'whitespace', [ '', '' ] ) + .data( 'attributes', '' ); + if ( selection !== '' ) { + var matches, text; + if ( ( matches = selection.match( /^(\s*)]*)>([^<]*)<\/ref\>(\s*)$/ ) ) ) { + text = matches[3]; + // Preserve whitespace when replacing + $( '#wikieditor-toolbar-reference-dialog' ) + .data( 'whitespace', [ matches[1], matches[4] ] ); + $( '#wikieditor-toolbar-reference-dialog' ).data( 'attributes', matches[2] ); + } else { + text = selection; } - }); - - // Make tabbing to a button and pressing - // Enter do what people expect - $(this).closest( '.ui-dialog' ).find( 'button' ).focus( function() { - $(this).closest( '.ui-dialog' ).data( 'dialogaction', this ); - }); + $( '#wikieditor-toolbar-reference-text' ).val( text ); + } + if ( !( $( this ).data( 'dialogkeypressset' ) ) ) { + $( this ).data( 'dialogkeypressset', true ); + // Execute the action associated with the first button + // when the user presses Enter + $( this ).closest( '.ui-dialog' ).keypress( function ( e ) { + if ( ( e.keyCode || e.which ) == 13 ) { + var button = $( this ).data( 'dialogaction' ) || $( this ).find( 'button:first' ); + button.click(); + e.preventDefault(); + } + } ); + // Make tabbing to a button and pressing + // Enter do what people expect + $( this ).closest( '.ui-dialog' ).find( 'button' ).focus( function () { + $( this ).closest( '.ui-dialog' ).data( 'dialogaction', this ); + } ); + } } } - } - }, - 'search-and-replace': { - 'browsers': { - // Left-to-right languages - 'ltr': { - 'msie': false, - 'firefox': [['>=', 2]], - 'opera': false, - 'safari': [['>=', 3]], - 'chrome': [['>=', 3]] - }, - // Right-to-left languages - 'rtl': { - 'msie': false, - 'firefox': [['>=', 2]], - 'opera': false, - 'safari': [['>=', 3]], - 'chrome': [['>=', 3]] - } }, - titleMsg: 'wikieditor-toolbar-tool-replace-title', - id: 'wikieditor-toolbar-replace-dialog', - html: '\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ -
', - init: function() { - $(this).find( '[rel]' ).each( function() { - $(this).text( mediaWiki.msg( $(this).attr( 'rel' ) ) ); - }); - // Set tabindexes on form fields - $.wikiEditor.modules.dialogs.fn.setTabindexes( $(this).find( 'input' ).not( '[tabindex]' ) ); + 'insert-table': { + titleMsg: 'wikieditor-toolbar-tool-table-title', + id: 'wikieditor-toolbar-table-dialog', + // FIXME: Localize 'x'? + html: '\ +
\ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
\ +
\ +
\ + \ +
\ +
\ +
\ + \ +
\ +
\ +
\ +
\ + \ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
\ +
\ +
', + init: function () { + $(this).find( '[rel]' ).each( function () { + $(this).text( mw.msg( $(this).attr( 'rel' ) ) ); + }); + // Set tabindexes on form fields + $.wikiEditor.modules.dialogs.fn.setTabindexes( $(this).find( 'input' ).not( '[tabindex]' ) ); - // TODO: Find a cleaner way to share this function - $(this).data( 'replaceCallback', function( mode ) { - $( '#wikieditor-toolbar-replace-nomatch, #wikieditor-toolbar-replace-success, #wikieditor-toolbar-replace-emptysearch, #wikieditor-toolbar-replace-invalidregex' ).hide(); + $( '#wikieditor-toolbar-table-dimensions-rows' ).val( 3 ); + $( '#wikieditor-toolbar-table-dimensions-columns' ).val( 3 ); + $( '#wikieditor-toolbar-table-wikitable' ).click( function () { + $( '.wikieditor-toolbar-table-preview' ).toggleClass( 'wikitable' ); + }); - // Search string cannot be empty - var searchStr = $( '#wikieditor-toolbar-replace-search' ).val(); - if ( searchStr == '' ) { - $( '#wikieditor-toolbar-replace-emptysearch' ).show(); - return; - } + // Hack for sortable preview: dynamically adding + // sortable class doesn't work, so we use a clone + $( '#wikieditor-toolbar-table-preview' ) + .clone() + .attr( 'id', 'wikieditor-toolbar-table-preview2' ) + .addClass( 'sortable' ) + .insertAfter( $( '#wikieditor-toolbar-table-preview' ) ) + .hide(); - // Replace string can be empty - var replaceStr = $( '#wikieditor-toolbar-replace-replace' ).val(); + mw.loader.using( 'jquery.tablesorter', function () { + $( '#wikieditor-toolbar-table-preview2' ).tablesorter(); + }); - // Prepare the regular expression flags - var flags = 'm'; - var matchCase = $( '#wikieditor-toolbar-replace-case' ).is( ':checked' ); - if ( !matchCase ) { - flags += 'i'; - } - var isRegex = $( '#wikieditor-toolbar-replace-regex' ).is( ':checked' ); - if ( !isRegex ) { - searchStr = $.escapeRE( searchStr ); - } - if ( mode == 'replaceAll' ) { - flags += 'g'; - } - - try { - var regex = new RegExp( searchStr, flags ); - } catch( e ) { - $( '#wikieditor-toolbar-replace-invalidregex' ) - .text( mediaWiki.msg( 'wikieditor-toolbar-tool-replace-invalidregex', - e.message ) ) + $( '#wikieditor-toolbar-table-sortable' ).click( function () { + // Swap the currently shown one clone with the other one + $( '#wikieditor-toolbar-table-preview' ) + .hide() + .attr( 'id', 'wikieditor-toolbar-table-preview3' ); + $( '#wikieditor-toolbar-table-preview2' ) + .attr( 'id', 'wikieditor-toolbar-table-preview' ) .show(); - return; - } + $( '#wikieditor-toolbar-table-preview3' ).attr( 'id', 'wikieditor-toolbar-table-preview2' ); + }); - var $textarea = $(this).data( 'context' ).$textarea; - var text = $textarea.textSelection( 'getContents' ); - var match = false; - var offset, textRemainder; - if ( mode != 'replaceAll' ) { - if (mode == 'replace') { - offset = $(this).data( 'matchIndex' ); - } else { - offset = $(this).data( 'offset' ); + $( '#wikieditor-toolbar-table-dimensions-header' ).click( function () { + // Instead of show/hiding, switch the HTML around + // We do this because the sortable tables script styles the first row, + // visible or not + var headerHTML = $( '.wikieditor-toolbar-table-preview-header' ).html(); + var hiddenHTML = $( '.wikieditor-toolbar-table-preview-hidden' ).html(); + $( '.wikieditor-toolbar-table-preview-header' ).html( hiddenHTML ); + $( '.wikieditor-toolbar-table-preview-hidden' ).html( headerHTML ); + if ( typeof jQuery.fn.tablesorter == 'function' ) { + $( '#wikieditor-toolbar-table-preview, #wikieditor-toolbar-table-preview2' ) + .filter( '.sortable' ) + .tablesorter(); } - textRemainder = text.substr( offset ); - match = textRemainder.match( regex ); - } - if ( !match ) { - // Search hit BOTTOM, continuing at TOP - // TODO: Add a "Wrap around" option. - offset = 0; - textRemainder = text; - match = textRemainder.match( regex ); - } - - if ( !match ) { - $( '#wikieditor-toolbar-replace-nomatch' ).show(); - } else if ( mode == 'replaceAll' ) { - // Instead of using repetitive .match() calls, we use one .match() call with /g - // and indexOf() followed by substr() to find the offsets. This is actually - // faster because our indexOf+substr loop is faster than a match loop, and the - // /g match is so ridiculously fast that it's negligible. - // FIXME: Repetitively calling encapsulateSelection() is probably the best strategy - // in Firefox/Webkit, but in IE replacing the entire content once is better. - var index; - for ( var i = 0; i < match.length; i++ ) { - index = textRemainder.indexOf( match[i] ); - if ( index == -1 ) { - // This shouldn't happen - break; + }); + }, + dialog: { + resizable: false, + dialogClass: 'wikiEditor-toolbar-dialog', + width: 590, + buttons: { + 'wikieditor-toolbar-tool-table-insert': function () { + var rowsVal = $( '#wikieditor-toolbar-table-dimensions-rows' ).val(); + var colsVal = $( '#wikieditor-toolbar-table-dimensions-columns' ).val(); + var rows = parseInt( rowsVal, 10 ); + var cols = parseInt( colsVal, 10 ); + var header = $( '#wikieditor-toolbar-table-dimensions-header' ).is( ':checked' ) ? 1 : 0; + if ( isNaN( rows ) || isNaN( cols ) || rows !== rowsVal || cols !== colsVal ) { + alert( mw.msg( 'wikieditor-toolbar-tool-table-invalidnumber' ) ); + return; } - var matchedText = textRemainder.substr( index, match[i].length ); - textRemainder = textRemainder.substr( index + match[i].length ); + if ( rows + header === 0 || cols === 0 ) { + alert( mw.msg( 'wikieditor-toolbar-tool-table-zero' ) ); + return; + } + if ( rows * cols > 1000 ) { + alert( mw.msg( 'wikieditor-toolbar-tool-table-toomany', 1000 ) ); + return; + } + var headerText = mw.msg( 'wikieditor-toolbar-tool-table-example-header' ); + var normalText = mw.msg( 'wikieditor-toolbar-tool-table-example' ); + var table = ""; + for ( var r = 0; r < rows + header; r++ ) { + table += "|-\n"; + for ( var c = 0; c < cols; c++ ) { + var isHeader = ( header && r === 0 ); + var delim = isHeader ? '!' : '|'; + if ( c > 0 ) { + delim += delim; + } + table += delim + ' ' + ( isHeader ? headerText : normalText ) + ' '; + } + // Replace trailing space by newline + // table[table.length - 1] is read-only + table = table.substr( 0, table.length - 1 ) + "\n"; + } + var classes = []; + if ( $( '#wikieditor-toolbar-table-wikitable' ).is( ':checked' ) ) + classes.push( 'wikitable' ); + if ( $( '#wikieditor-toolbar-table-sortable' ).is( ':checked' ) ) + classes.push( 'sortable' ); + var classStr = classes.length > 0 ? ' class="' + classes.join( ' ' ) + '"' : ''; + $(this).dialog( 'close' ); + $.wikiEditor.modules.toolbar.fn.doAction( + $(this).data( 'context' ), + { + type: 'replace', + options: { + pre: '{|' + classStr + "\n", + peri: table, + post: '|}', + ownline: true + } + }, + $(this) + ); - var start = index + offset; - var end = start + match[i].length; - // Make regex placeholder substitution ($1) work - var replace = isRegex ? matchedText.replace( regex, replaceStr ) : replaceStr; - var newEnd = start + replace.length; - $textarea - .textSelection( 'setSelection', { 'start': start, 'end': end } ) - .textSelection( 'encapsulateSelection', { - 'peri': replace, - 'replace': true } ) - .textSelection( 'setSelection', { 'start': start, 'end': newEnd } ); - offset = newEnd; + // Restore form state + $( '#wikieditor-toolbar-table-dimensions-rows' ).val( 3 ); + $( '#wikieditor-toolbar-table-dimensions-columns' ).val( 3 ); + // Simulate clicks instead of setting values, so the according + // actions are performed + if ( !$( '#wikieditor-toolbar-table-dimensions-header' ).is( ':checked' ) ) + $( '#wikieditor-toolbar-table-dimensions-header' ).click(); + if ( !$( '#wikieditor-toolbar-table-wikitable' ).is( ':checked' ) ) + $( '#wikieditor-toolbar-table-wikitable' ).click(); + if ( $( '#wikieditor-toolbar-table-sortable' ).is( ':checked' ) ) + $( '#wikieditor-toolbar-table-sortable' ).click(); + }, + 'wikieditor-toolbar-tool-table-cancel': function () { + $(this).dialog( 'close' ); } - $( '#wikieditor-toolbar-replace-success' ) - .text( mediaWiki.msg( 'wikieditor-toolbar-tool-replace-success', match.length ) ) - .show(); - $(this).data( 'offset', 0 ); - } else { - var start, end; + }, + open: function () { + $( '#wikieditor-toolbar-table-dimensions-rows' ).focus(); + if ( !( $(this).data( 'dialogkeypressset' ) ) ) { + $(this).data( 'dialogkeypressset', true ); + // Execute the action associated with the first button + // when the user presses Enter + $(this).closest( '.ui-dialog' ).keypress( function ( e ) { + if ( ( e.keyCode || e.which ) == 13 ) { + var button = $(this).data( 'dialogaction' ) || $(this).find( 'button:first' ); + button.click(); + e.preventDefault(); + } + }); - if ( mode == 'replace' ) { - var actualReplacement; + // Make tabbing to a button and pressing + // Enter do what people expect + $(this).closest( '.ui-dialog' ).find( 'button' ).focus( function () { + $(this).closest( '.ui-dialog' ).data( 'dialogaction', this ); + }); + } + } + } + }, + 'search-and-replace': { + 'browsers': { + // Left-to-right languages + 'ltr': { + 'msie': false, + 'firefox': [['>=', 2]], + 'opera': false, + 'safari': [['>=', 3]], + 'chrome': [['>=', 3]] + }, + // Right-to-left languages + 'rtl': { + 'msie': false, + 'firefox': [['>=', 2]], + 'opera': false, + 'safari': [['>=', 3]], + 'chrome': [['>=', 3]] + } + }, + titleMsg: 'wikieditor-toolbar-tool-replace-title', + id: 'wikieditor-toolbar-replace-dialog', + html: '\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
', + init: function () { + $(this).find( '[rel]' ).each( function () { + $(this).text( mw.msg( $(this).attr( 'rel' ) ) ); + }); + // Set tabindexes on form fields + $.wikiEditor.modules.dialogs.fn.setTabindexes( $(this).find( 'input' ).not( '[tabindex]' ) ); - if (isRegex) { - // If backreferences (like $1) are used, the actual actual replacement string will be different - actualReplacement = match[0].replace( regex, replaceStr ); + // TODO: Find a cleaner way to share this function + $(this).data( 'replaceCallback', function ( mode ) { + var offset, textRemainder, regex, index, i, + start, end; + + $( '#wikieditor-toolbar-replace-nomatch, #wikieditor-toolbar-replace-success, #wikieditor-toolbar-replace-emptysearch, #wikieditor-toolbar-replace-invalidregex' ).hide(); + + // Search string cannot be empty + var searchStr = $( '#wikieditor-toolbar-replace-search' ).val(); + if ( searchStr === '' ) { + $( '#wikieditor-toolbar-replace-emptysearch' ).show(); + return; + } + + // Replace string can be empty + var replaceStr = $( '#wikieditor-toolbar-replace-replace' ).val(); + + // Prepare the regular expression flags + var flags = 'm'; + var matchCase = $( '#wikieditor-toolbar-replace-case' ).is( ':checked' ); + if ( !matchCase ) { + flags += 'i'; + } + var isRegex = $( '#wikieditor-toolbar-replace-regex' ).is( ':checked' ); + if ( !isRegex ) { + searchStr = $.escapeRE( searchStr ); + } + if ( mode === 'replaceAll' ) { + flags += 'g'; + } + + try { + regex = new RegExp( searchStr, flags ); + } catch ( e ) { + $( '#wikieditor-toolbar-replace-invalidregex' ) + .text( mw.msg( 'wikieditor-toolbar-tool-replace-invalidregex', + e.message ) ) + .show(); + return; + } + + var $textarea = $(this).data( 'context' ).$textarea; + var text = $textarea.textSelection( 'getContents' ); + var match = false; + if ( mode !== 'replaceAll' ) { + if ( mode === 'replace' ) { + offset = $(this).data( 'matchIndex' ); } else { - actualReplacement = replaceStr; + offset = $(this).data( 'offset' ); } - - if (match) { - // Do the replacement - $textarea.textSelection( 'encapsulateSelection', { - 'peri': actualReplacement, - 'replace': true } ); - // Reload the text after replacement - text = $textarea.textSelection( 'getContents' ); - } - - // Find the next instance - offset = offset + match[0].length + actualReplacement.length; textRemainder = text.substr( offset ); match = textRemainder.match( regex ); - - if (match) { - start = offset + match.index; - end = start + match[0].length; - } else { - // If no new string was found, try searching from the beginning. - // TODO: Add a "Wrap around" option. - textRemainder = text; - match = textRemainder.match( regex ); - if (match) { - start = match.index; - end = start + match[0].length; - } else { - // Give up - start = 0; - end = 0; - } - } - } else { - start = offset + match.index; - end = start + match[0].length; + } + if ( !match ) { + // Search hit BOTTOM, continuing at TOP + // TODO: Add a "Wrap around" option. + offset = 0; + textRemainder = text; + match = textRemainder.match( regex ); } - $( this ).data( 'matchIndex', start); + if ( !match ) { + $( '#wikieditor-toolbar-replace-nomatch' ).show(); + } else if ( mode == 'replaceAll' ) { + // Instead of using repetitive .match() calls, we use one .match() call with /g + // and indexOf() followed by substr() to find the offsets. This is actually + // faster because our indexOf+substr loop is faster than a match loop, and the + // /g match is so ridiculously fast that it's negligible. + // FIXME: Repetitively calling encapsulateSelection() is probably the best strategy + // in Firefox/Webkit, but in IE replacing the entire content once is better. + for ( i = 0; i < match.length; i++ ) { + index = textRemainder.indexOf( match[i] ); + if ( index === -1 ) { + // This shouldn't happen + break; + } + var matchedText = textRemainder.substr( index, match[i].length ); + textRemainder = textRemainder.substr( index + match[i].length ); - $textarea.textSelection( 'setSelection', { - 'start': start, - 'end': end - } ); - $textarea.textSelection( 'scrollToCaretPosition' ); - $( this ).data( 'offset', end ); - var context = $( this ).data( 'context' ); - var textbox = typeof context.$iframe != 'undefined' ? - context.$iframe[0].contentWindow : $textarea[0]; - textbox.focus(); - } - }); - }, - dialog: { - width: 500, - dialogClass: 'wikiEditor-toolbar-dialog', - buttons: { - 'wikieditor-toolbar-tool-replace-button-findnext': function( e ) { - $(this).closest( '.ui-dialog' ).data( 'dialogaction', e.target ); - $(this).data( 'replaceCallback' ).call( this, 'find' ); - }, - 'wikieditor-toolbar-tool-replace-button-replace': function( e ) { - $(this).closest( '.ui-dialog' ).data( 'dialogaction', e.target ); - $(this).data( 'replaceCallback' ).call( this, 'replace' ); - }, - 'wikieditor-toolbar-tool-replace-button-replaceall': function( e ) { - $(this).closest( '.ui-dialog' ).data( 'dialogaction', e.target ); - $(this).data( 'replaceCallback' ).call( this, 'replaceAll' ); - }, - 'wikieditor-toolbar-tool-replace-close': function() { - $(this).dialog( 'close' ); - } - }, - open: function() { - $(this).data( 'offset', 0 ); - $(this).data( 'matchIndex', 0 ); - - $( '#wikieditor-toolbar-replace-search' ).focus(); - $( '#wikieditor-toolbar-replace-nomatch, #wikieditor-toolbar-replace-success, #wikieditor-toolbar-replace-emptysearch, #wikieditor-toolbar-replace-invalidregex' ).hide(); - if ( !( $(this).data( 'onetimeonlystuff' ) ) ) { - $(this).data( 'onetimeonlystuff', true ); - // Execute the action associated with the first button - // when the user presses Enter - $(this).closest( '.ui-dialog' ).keypress( function( e ) { - if ( ( e.keyCode || e.which ) == 13 ) { - var button = $(this).data( 'dialogaction' ) || $(this).find( 'button:first' ); - button.click(); - e.preventDefault(); + start = index + offset; + end = start + match[i].length; + // Make regex placeholder substitution ($1) work + var replace = isRegex ? matchedText.replace( regex, replaceStr ) : replaceStr; + var newEnd = start + replace.length; + $textarea + .textSelection( 'setSelection', { 'start': start, 'end': end } ) + .textSelection( 'encapsulateSelection', { + 'peri': replace, + 'replace': true } ) + .textSelection( 'setSelection', { 'start': start, 'end': newEnd } ); + offset = newEnd; } - }); - // Make tabbing to a button and pressing - // Enter do what people expect - $(this).closest( '.ui-dialog' ).find( 'button' ).focus( function() { - $(this).closest( '.ui-dialog' ).data( 'dialogaction', this ); - }); - } - var dialog = $(this).closest( '.ui-dialog' ); - var that = this; - var context = $(this).data( 'context' ); - var textbox = typeof context.$iframe != 'undefined' ? - context.$iframe[0].contentWindow.document : context.$textarea; + $( '#wikieditor-toolbar-replace-success' ) + .text( mw.msg( 'wikieditor-toolbar-tool-replace-success', match.length ) ) + .show(); + $(this).data( 'offset', 0 ); + } else { - $( textbox ) - .bind( 'keypress.srdialog', function( e ) { - if ( ( e.keyCode || e.which ) == 13 ) { - // Enter - var button = dialog.data( 'dialogaction' ) || dialog.find( 'button:first' ); - button.click(); - e.preventDefault(); - } else if ( ( e.keyCode || e.which ) == 27 ) { - // Escape - $(that).dialog( 'close' ); + if ( mode == 'replace' ) { + var actualReplacement; + + if (isRegex) { + // If backreferences (like $1) are used, the actual actual replacement string will be different + actualReplacement = match[0].replace( regex, replaceStr ); + } else { + actualReplacement = replaceStr; + } + + if (match) { + // Do the replacement + $textarea.textSelection( 'encapsulateSelection', { + 'peri': actualReplacement, + 'replace': true } ); + // Reload the text after replacement + text = $textarea.textSelection( 'getContents' ); + } + + // Find the next instance + offset = offset + match[0].length + actualReplacement.length; + textRemainder = text.substr( offset ); + match = textRemainder.match( regex ); + + if (match) { + start = offset + match.index; + end = start + match[0].length; + } else { + // If no new string was found, try searching from the beginning. + // TODO: Add a "Wrap around" option. + textRemainder = text; + match = textRemainder.match( regex ); + if (match) { + start = match.index; + end = start + match[0].length; + } else { + // Give up + start = 0; + end = 0; + } + } + } else { + start = offset + match.index; + end = start + match[0].length; } - }); + + $( this ).data( 'matchIndex', start); + + $textarea.textSelection( 'setSelection', { + 'start': start, + 'end': end + } ); + $textarea.textSelection( 'scrollToCaretPosition' ); + $( this ).data( 'offset', end ); + var context = $( this ).data( 'context' ); + var textbox = typeof context.$iframe !== 'undefined' ? + context.$iframe[0].contentWindow : $textarea[0]; + textbox.focus(); + } + }); }, - close: function() { - var context = $(this).data( 'context' ); - var textbox = typeof context.$iframe != 'undefined' ? - context.$iframe[0].contentWindow.document : context.$textarea; - $( textbox ).unbind( 'keypress.srdialog' ); - $(this).closest( '.ui-dialog' ).data( 'dialogaction', false ); + dialog: { + width: 500, + dialogClass: 'wikiEditor-toolbar-dialog', + buttons: { + 'wikieditor-toolbar-tool-replace-button-findnext': function ( e ) { + $(this).closest( '.ui-dialog' ).data( 'dialogaction', e.target ); + $(this).data( 'replaceCallback' ).call( this, 'find' ); + }, + 'wikieditor-toolbar-tool-replace-button-replace': function ( e ) { + $(this).closest( '.ui-dialog' ).data( 'dialogaction', e.target ); + $(this).data( 'replaceCallback' ).call( this, 'replace' ); + }, + 'wikieditor-toolbar-tool-replace-button-replaceall': function ( e ) { + $(this).closest( '.ui-dialog' ).data( 'dialogaction', e.target ); + $(this).data( 'replaceCallback' ).call( this, 'replaceAll' ); + }, + 'wikieditor-toolbar-tool-replace-close': function () { + $(this).dialog( 'close' ); + } + }, + open: function () { + $(this).data( 'offset', 0 ); + $(this).data( 'matchIndex', 0 ); + + $( '#wikieditor-toolbar-replace-search' ).focus(); + $( '#wikieditor-toolbar-replace-nomatch, #wikieditor-toolbar-replace-success, #wikieditor-toolbar-replace-emptysearch, #wikieditor-toolbar-replace-invalidregex' ).hide(); + if ( !( $(this).data( 'onetimeonlystuff' ) ) ) { + $(this).data( 'onetimeonlystuff', true ); + // Execute the action associated with the first button + // when the user presses Enter + $(this).closest( '.ui-dialog' ).keypress( function ( e ) { + if ( ( e.keyCode || e.which ) == 13 ) { + var button = $(this).data( 'dialogaction' ) || $(this).find( 'button:first' ); + button.click(); + e.preventDefault(); + } + }); + // Make tabbing to a button and pressing + // Enter do what people expect + $(this).closest( '.ui-dialog' ).find( 'button' ).focus( function () { + $(this).closest( '.ui-dialog' ).data( 'dialogaction', this ); + }); + } + var dialog = $(this).closest( '.ui-dialog' ); + var that = this; + var context = $(this).data( 'context' ); + var textbox = typeof context.$iframe !== 'undefined' ? + context.$iframe[0].contentWindow.document : context.$textarea; + + $( textbox ) + .bind( 'keypress.srdialog', function ( e ) { + if ( e.which == 13 ) { + // Enter + var button = dialog.data( 'dialogaction' ) || dialog.find( 'button:first' ); + button.click(); + e.preventDefault(); + } else if ( e.which == 27 ) { + // Escape + $(that).dialog( 'close' ); + } + }); + }, + close: function () { + var context = $(this).data( 'context' ); + var textbox = typeof context.$iframe !== 'undefined' ? + context.$iframe[0].contentWindow.document : context.$textarea; + $( textbox ).unbind( 'keypress.srdialog' ); + $(this).closest( '.ui-dialog' ).data( 'dialogaction', false ); + } } } - } - } }; -} + } }; + } -}; } ) ( jQuery ); +}; + +}( jQuery, mediaWiki ) ); diff --git a/modules/jquery.wikiEditor.dialogs.css b/modules/jquery.wikiEditor.dialogs.css index 2c3d36a6..b97ce7e3 100644 --- a/modules/jquery.wikiEditor.dialogs.css +++ b/modules/jquery.wikiEditor.dialogs.css @@ -1,56 +1,68 @@ -/* +/** * CSS for WikiEditor Dialogs jQuery plugin */ .wikiEditor-toolbar-dialog table { margin-top: 0.75em; } + .wikiEditor-toolbar-dialog table td { padding: 0.5em; height: 3em; overflow: visible; } + /* Put suggestions (default z-index 99) on top of dialogs (z-index 1002) */ div.suggestions { z-index: 1099; } + .wikiEditor-toolbar-dialog table td { padding: 0 !important; } + .wikiEditor-toolbar-dialog .ui-dialog-content fieldset { border: none !important; margin: 0 !important; padding: 0 !important; } + .wikiEditor-toolbar-dialog .ui-widget-header { border-bottom:1px solid #6bc8f3 !important; } + .wikiEditor-toolbar-dialog .ui-dialog-content input[type=text] { -moz-box-sizing: border-box; -ms-box-sizing: border-box; -webkit-box-sizing: border-box; -khtml-box-sizing: border-box; } + .wikiEditor-toolbar-dialog .ui-dialog-content input[type="radio"], .wikiEditor-toolbar-dialog .ui-dialog-content input[type="checkbox"] { margin-left: 0; } + .wikiEditor-toolbar-dialog .ui-dialog-titlebar-close { padding: 0; } body .wikiEditor-toolbar-dialog .ui-dialog-titlebar-close { right: 0.9em; } + .wikieditor-toolbar-field-wrapper { padding: 0 0 25px 0; } + .wikieditor-toolbar-floated-field-wrapper { float: left; margin-right: 2em; } + .wikieditor-toolbar-dialog-hint { color: #999999; } + .wikiEditor-toolbar-dialog { border: none; } \ No newline at end of file diff --git a/modules/jquery.wikiEditor.dialogs.js b/modules/jquery.wikiEditor.dialogs.js index 6f5404f5..8a629e14 100644 --- a/modules/jquery.wikiEditor.dialogs.js +++ b/modules/jquery.wikiEditor.dialogs.js @@ -1,217 +1,231 @@ /** * Dialog Module for wikiEditor */ -( function( $ ) { $.wikiEditor.modules.dialogs = { +( function ( $, mw ) { -/** - * Compatability map - */ -'browsers': { - // Left-to-right languages - 'ltr': { - 'msie': [['>=', 7]], - // jQuery UI appears to be broken in FF 2.0 - 2.0.0.4 - 'firefox': [ - ['>=', 2], ['!=', '2.0'], ['!=', '2.0.0.1'], ['!=', '2.0.0.2'], ['!=', '2.0.0.3'], ['!=', '2.0.0.4'] - ], - 'opera': [['>=', 9.6]], - 'safari': [['>=', 3]], - 'chrome': [['>=', 3]] - }, - // Right-to-left languages - 'rtl': { - 'msie': [['>=', 7]], - // jQuery UI appears to be broken in FF 2.0 - 2.0.0.4 - 'firefox': [ - ['>=', 2], ['!=', '2.0'], ['!=', '2.0.0.1'], ['!=', '2.0.0.2'], ['!=', '2.0.0.3'], ['!=', '2.0.0.4'] - ], - 'opera': [['>=', 9.6]], - 'safari': [['>=', 3]], - 'chrome': [['>=', 3]] - } -}, -/** - * API accessible functions - */ -api: { - addDialog: function( context, data ) { - $.wikiEditor.modules.dialogs.fn.create( context, data ) - }, - openDialog: function( context, module ) { - if ( module in $.wikiEditor.modules.dialogs.modules ) { - var mod = $.wikiEditor.modules.dialogs.modules[module]; - var $dialog = $( '#' + mod.id ); - if ( $dialog.length == 0 ) { - $.wikiEditor.modules.dialogs.fn.reallyCreate( context, mod, module ); - $dialog = $( '#' + mod.id ); - } +$.wikiEditor.modules.dialogs = { - // Workaround for bug in jQuery UI: close button in top right retains focus - $dialog.closest( '.ui-dialog' ) - .find( '.ui-dialog-titlebar-close' ) - .removeClass( 'ui-state-focus' ); - - $dialog.dialog( 'open' ); - } - }, - closeDialog: function( context, module ) { - if ( module in $.wikiEditor.modules.dialogs.modules ) { - $( '#' + $.wikiEditor.modules.dialogs.modules[module].id ).dialog( 'close' ); - } - } -}, -/** - * Internally used functions - */ -fn: { /** - * Creates a dialog module within a wikiEditor - * - * @param {Object} context Context object of editor to create module in - * @param {Object} config Configuration object to create module from + * Compatability map */ - create: function( context, config ) { - // Defer building of modules, but do check whether they need the iframe rightaway - for ( var mod in config ) { - var module = config[mod]; - // Only create the dialog if it's supported, isn't filtered and doesn't exist yet - var filtered = false; - if ( typeof module.filters != 'undefined' ) { - for ( var i = 0; i < module.filters.length; i++ ) { - if ( $( module.filters[i] ).length == 0 ) { - filtered = true; - break; + browsers: { + // Left-to-right languages + ltr: { + msie: [['>=', 7]], + // jQuery UI appears to be broken in FF 2.0 - 2.0.0.4 + firefox: [ + ['>=', 2], ['!=', '2.0'], ['!=', '2.0.0.1'], ['!=', '2.0.0.2'], ['!=', '2.0.0.3'], ['!=', '2.0.0.4'] + ], + opera: [['>=', 9.6]], + safari: [['>=', 3]], + chrome: [['>=', 3]] + }, + // Right-to-left languages + rtl: { + msie: [['>=', 7]], + // jQuery UI appears to be broken in FF 2.0 - 2.0.0.4 + firefox: [ + ['>=', 2], ['!=', '2.0'], ['!=', '2.0.0.1'], ['!=', '2.0.0.2'], ['!=', '2.0.0.3'], ['!=', '2.0.0.4'] + ], + opera: [['>=', 9.6]], + safari: [['>=', 3]], + chrome: [['>=', 3]] + } + }, + + /** + * API accessible functions + */ + api: { + addDialog: function ( context, data ) { + $.wikiEditor.modules.dialogs.fn.create( context, data ); + }, + openDialog: function ( context, module ) { + if ( module in $.wikiEditor.modules.dialogs.modules ) { + var mod = $.wikiEditor.modules.dialogs.modules[module]; + var $dialog = $( '#' + mod.id ); + if ( $dialog.length === 0 ) { + $.wikiEditor.modules.dialogs.fn.reallyCreate( context, mod, module ); + $dialog = $( '#' + mod.id ); + } + + // Workaround for bug in jQuery UI: close button in top right retains focus + $dialog.closest( '.ui-dialog' ) + .find( '.ui-dialog-titlebar-close' ) + .removeClass( 'ui-state-focus' ); + + $dialog.dialog( 'open' ); + } + }, + closeDialog: function ( context, module ) { + if ( module in $.wikiEditor.modules.dialogs.modules ) { + $( '#' + $.wikiEditor.modules.dialogs.modules[module].id ).dialog( 'close' ); + } + } + }, + + /** + * Internally used functions + */ + fn: { + /** + * Creates a dialog module within a wikiEditor + * + * @param {Object} context Context object of editor to create module in + * @param {Object} config Configuration object to create module from + */ + create: function ( context, config ) { + var mod, module, filtered, i, $existingDialog; + + // Defer building of modules, but do check whether they need the iframe rightaway + for ( mod in config ) { + module = config[mod]; + // Only create the dialog if it's supported, isn't filtered and doesn't exist yet + filtered = false; + if ( typeof module.filters != 'undefined' ) { + for ( i = 0; i < module.filters.length; i++ ) { + if ( $( module.filters[i] ).length === 0 ) { + filtered = true; + break; + } + } + } + // If the dialog already exists, but for another textarea, simply remove it + $existingDialog = $( '#' + module.id ); + if ( $existingDialog.length > 0 && $existingDialog.data( 'context' ).$textarea != context.$textarea ) { + $existingDialog.remove(); + } + // Re-select from the DOM, we might have removed the dialog just now + $existingDialog = $( '#' + module.id ); + if ( !filtered && $.wikiEditor.isSupported( module ) && $existingDialog.length === 0 ) { + $.wikiEditor.modules.dialogs.modules[mod] = module; + // If this dialog requires the iframe, set it up + if ( typeof context.$iframe === 'undefined' && $.wikiEditor.isRequired( module, 'iframe' ) ) { + context.fn.setupIframe(); + } + context.$textarea.trigger( 'wikiEditor-dialogs-setup-' + mod ); + // If this dialog requires immediate creation, create it now + if ( typeof module.immediateCreate !== 'undefined' && module.immediateCreate ) { + $.wikiEditor.modules.dialogs.fn.reallyCreate( context, module, mod ); } } } - // If the dialog already exists, but for another textarea, simply remove it - var $existingDialog = $( '#' + module.id ); - if ( $existingDialog.length > 0 && $existingDialog.data( 'context' ).$textarea != context.$textarea ) { - $existingDialog.remove(); + }, + + /** + * Build the actual dialog. This done on-demand rather than in create() + * @param {Object} context Context object of editor dialog belongs to + * @param {Object} module Dialog module object + * @param {String} name Dialog name (key in $.wikiEditor.modules.dialogs.modules) + */ + reallyCreate: function ( context, module, name ) { + var msg, + configuration = module.dialog; + // Add some stuff to configuration + configuration.bgiframe = true; + configuration.autoOpen = false; + configuration.modal = true; + configuration.title = $.wikiEditor.autoMsg( module, 'title' ); + // Transform messages in keys + // Stupid JS won't let us do stuff like + // foo = { mw.msg( 'bar' ): baz } + configuration.newButtons = {}; + for ( msg in configuration.buttons ) { + configuration.newButtons[mw.msg( msg )] = configuration.buttons[msg]; } - // Re-select from the DOM, we might have removed the dialog just now - $existingDialog = $( '#' + module.id ); - if ( !filtered && $.wikiEditor.isSupported( module ) && $existingDialog.size() === 0 ) { - $.wikiEditor.modules.dialogs.modules[mod] = module; - // If this dialog requires the iframe, set it up - if ( typeof context.$iframe == 'undefined' && $.wikiEditor.isRequired( module, 'iframe' ) ) { - context.fn.setupIframe(); - } - context.$textarea.trigger( 'wikiEditor-dialogs-setup-' + mod ); - // If this dialog requires immediate creation, create it now - if ( typeof module.immediateCreate !== 'undefined' && module.immediateCreate ) { - $.wikiEditor.modules.dialogs.fn.reallyCreate( context, module, mod ); - } + configuration.buttons = configuration.newButtons; + // Create the dialog
+ var dialogDiv = $( '
' ) + .attr( 'id', module.id ) + .html( module.html ) + .data( 'context', context ) + .appendTo( $( 'body' ) ) + .each( module.init ) + .dialog( configuration ); + // Set tabindexes on buttons added by .dialog() + $.wikiEditor.modules.dialogs.fn.setTabindexes( dialogDiv.closest( '.ui-dialog' ) + .find( 'button' ).not( '[tabindex]' ) ); + if ( !( 'resizeme' in module ) || module.resizeme ) { + dialogDiv + .bind( 'dialogopen', $.wikiEditor.modules.dialogs.fn.resize ) + .find( '.ui-tabs' ).bind( 'tabsshow', function () { + $(this).closest( '.ui-dialog-content' ).each( + $.wikiEditor.modules.dialogs.fn.resize ); + }); } + dialogDiv.bind( 'dialogclose', function () { + context.fn.restoreSelection(); + } ); + + // Let the outside world know we set up this dialog + context.$textarea.trigger( 'wikiEditor-dialogs-loaded-' + name ); + }, + + /** + * Resize a dialog so its contents fit + * + * Usage: dialog.each( resize ); or dialog.bind( 'blah', resize ); + * NOTE: This function assumes $.ui.dialog has already been loaded + */ + resize: function () { + var wrapper = $(this).closest( '.ui-dialog' ); + var oldWidth = wrapper.width(); + // Make sure elements don't wrapped so we get an accurate idea of whether they really fit. Also temporarily show + // hidden elements. Work around jQuery bug where
inside a dialog is both + // :visible and :hidden + var oldHidden = $(this).find( '*' ).not( ':visible' ); + // Save the style attributes of the hidden elements to restore them later. Calling hide() after show() messes up + // for elements hidden with a class + oldHidden.each( function () { + $(this).data( 'oldstyle', $(this).attr( 'style' ) ); + }); + oldHidden.show(); + var oldWS = $(this).css( 'white-space' ); + $(this).css( 'white-space', 'nowrap' ); + if ( wrapper.width() <= $(this).get(0).scrollWidth ) { + var thisWidth = $(this).data( 'thisWidth' ) ? $(this).data( 'thisWidth' ) : 0; + thisWidth = Math.max( $(this).get(0).width, thisWidth ); + $(this).width( thisWidth ); + $(this).data( 'thisWidth', thisWidth ); + var wrapperWidth = $(this).data( 'wrapperWidth' ) ? $(this).data( 'wrapperWidth' ) : 0; + wrapperWidth = Math.max( wrapper.get(0).scrollWidth, wrapperWidth ); + wrapper.width( wrapperWidth ); + $(this).data( 'wrapperWidth', wrapperWidth ); + $(this).dialog( { 'width': wrapper.width() } ); + wrapper.css( 'left', parseInt( wrapper.css( 'left' ), 10 ) - ( wrapper.width() - oldWidth ) / 2 ); + } + $(this).css( 'white-space', oldWS ); + oldHidden.each( function () { + $(this).attr( 'style', $(this).data( 'oldstyle' ) ); + }); + }, + /** + * Set the right tabindexes on elements in a dialog + * @param $elements Elements to set tabindexes on. If they already have tabindexes, this function can behave a bit weird + */ + setTabindexes: function ( $elements ) { + // Get the highest tab index + var tabIndex = $( document ).lastTabIndex() + 1; + $elements.each( function () { + $(this).attr( 'tabindex', tabIndex++ ); + } ); } }, - /** - * Build the actual dialog. This done on-demand rather than in create() - * @param {Object} context Context object of editor dialog belongs to - * @param {Object} module Dialog module object - * @param {String} name Dialog name (key in $.wikiEditor.modules.dialogs.modules) - */ - reallyCreate: function( context, module, name ) { - var configuration = module.dialog; - // Add some stuff to configuration - configuration.bgiframe = true; - configuration.autoOpen = false; - configuration.modal = true; - configuration.title = $.wikiEditor.autoMsg( module, 'title' ); - // Transform messages in keys - // Stupid JS won't let us do stuff like - // foo = { mediaWiki.msg( 'bar' ): baz } - configuration.newButtons = {}; - for ( var msg in configuration.buttons ) - configuration.newButtons[mediaWiki.msg( msg )] = configuration.buttons[msg]; - configuration.buttons = configuration.newButtons; - // Create the dialog
- var dialogDiv = $( '
' ) - .attr( 'id', module.id ) - .html( module.html ) - .data( 'context', context ) + + // This stuff is just hanging here, perhaps we could come up with a better home for this stuff + modules: {}, + + quickDialog: function ( body, settings ) { + $( '
' ) + .text( body ) .appendTo( $( 'body' ) ) - .each( module.init ) - .dialog( configuration ); - // Set tabindexes on buttons added by .dialog() - $.wikiEditor.modules.dialogs.fn.setTabindexes( dialogDiv.closest( '.ui-dialog' ) - .find( 'button' ).not( '[tabindex]' ) ); - if ( !( 'resizeme' in module ) || module.resizeme ) { - dialogDiv - .bind( 'dialogopen', $.wikiEditor.modules.dialogs.fn.resize ) - .find( '.ui-tabs' ).bind( 'tabsshow', function() { - $(this).closest( '.ui-dialog-content' ).each( - $.wikiEditor.modules.dialogs.fn.resize ); - }); - } - dialogDiv.bind( 'dialogclose', function() { - context.fn.restoreSelection(); - } ); - - // Let the outside world know we set up this dialog - context.$textarea.trigger( 'wikiEditor-dialogs-loaded-' + name ); - }, - /** - * Resize a dialog so its contents fit - * - * Usage: dialog.each( resize ); or dialog.bind( 'blah', resize ); - * NOTE: This function assumes $.ui.dialog has already been loaded - */ - resize: function() { - var wrapper = $(this).closest( '.ui-dialog' ); - var oldWidth = wrapper.width(); - // Make sure elements don't wrapped so we get an accurate idea of whether they really fit. Also temporarily show - // hidden elements. Work around jQuery bug where
inside a dialog is both - // :visible and :hidden - var oldHidden = $(this).find( '*' ).not( ':visible' ); - // Save the style attributes of the hidden elements to restore them later. Calling hide() after show() messes up - // for elements hidden with a class - oldHidden.each( function() { - $(this).data( 'oldstyle', $(this).attr( 'style' ) ); - }); - oldHidden.show(); - var oldWS = $(this).css( 'white-space' ); - $(this).css( 'white-space', 'nowrap' ); - if ( wrapper.width() <= $(this).get(0).scrollWidth ) { - var thisWidth = $(this).data( 'thisWidth' ) ? $(this).data( 'thisWidth' ) : 0; - thisWidth = Math.max( $(this).get(0).width, thisWidth ); - $(this).width( thisWidth ); - $(this).data( 'thisWidth', thisWidth ); - var wrapperWidth = $(this).data( 'wrapperWidth' ) ? $(this).data( 'wrapperWidth' ) : 0; - wrapperWidth = Math.max( wrapper.get(0).scrollWidth, wrapperWidth ); - wrapper.width( wrapperWidth ); - $(this).data( 'wrapperWidth', wrapperWidth ); - $(this).dialog( { 'width': wrapper.width() } ); - wrapper.css( 'left', parseInt( wrapper.css( 'left' ) ) - ( wrapper.width() - oldWidth ) / 2 ); - } - $(this).css( 'white-space', oldWS ); - oldHidden.each( function() { - $(this).attr( 'style', $(this).data( 'oldstyle' ) ); - }); - }, - /** - * Set the right tabindexes on elements in a dialog - * @param $elements Elements to set tabindexes on. If they already have tabindexes, this function can behave a bit weird - */ - setTabindexes: function( $elements ) { - // Get the highest tab index - var tabIndex = $( document ).lastTabIndex() + 1; - $elements.each( function() { - $(this).attr( 'tabindex', tabIndex++ ); - } ); + .dialog( $.extend( { + bgiframe: true, + modal: true + }, settings ) ) + .dialog( 'open' ); } -}, -// This stuff is just hanging here, perhaps we could come up with a better home for this stuff -modules: {}, -quickDialog: function( body, settings ) { - $( '
' ) - .text( body ) - .appendTo( $( 'body' ) ) - .dialog( $.extend( { - bgiframe: true, - modal: true - }, settings ) ) - .dialog( 'open' ); -} -}; } ) ( jQuery ); +}; + +}( jQuery, mediaWiki ) ); diff --git a/modules/jquery.wikiEditor.highlight.js b/modules/jquery.wikiEditor.highlight.js index 48140206..9d964fa7 100644 --- a/modules/jquery.wikiEditor.highlight.js +++ b/modules/jquery.wikiEditor.highlight.js @@ -1,357 +1,373 @@ /* Highlight module for wikiEditor */ -( function( $ ) { $.wikiEditor.modules.highlight = { +( function ( $ ) { -/** - * Core Requirements - */ -'req': [ 'iframe' ], -/** - * Configuration - */ -'cfg': { - 'styleVersion': 3 -}, -/** - * Internally used event handlers - */ -'evt': { - 'delayedChange': function( context, event ) { - if ( event.data.scope == 'realchange' ) { +$.wikiEditor.modules.highlight = { + + /** + * Core Requirements + */ + req: [ 'iframe' ], + + /** + * Configuration + */ + cfg: { + styleVersion: 3 + }, + + /** + * Internally used event handlers + */ + evt: { + delayedChange: function ( context, event ) { + if ( event.data.scope == 'realchange' ) { + $.wikiEditor.modules.highlight.fn.scan( context ); + $.wikiEditor.modules.highlight.fn.mark( context, event.data.scope ); + } + }, + ready: function ( context, event ) { $.wikiEditor.modules.highlight.fn.scan( context ); - $.wikiEditor.modules.highlight.fn.mark( context, event.data.scope ); + $.wikiEditor.modules.highlight.fn.mark( context, 'ready' ); } }, - 'ready': function( context, event ) { - $.wikiEditor.modules.highlight.fn.scan( context ); - $.wikiEditor.modules.highlight.fn.mark( context, 'ready' ); - } -}, -/** - * Internally used functions - */ -'fn': { + /** - * Creates a highlight module within a wikiEditor - * - * @param config Configuration object to create module from + * Internally used functions */ - 'create': function( context, config ) { - context.modules.highlight.markersStr = ''; - }, - /** - * Scans text division for tokens - * - * @param division - */ - 'scan': function( context, division ) { - // Remove all existing tokens - var tokenArray = context.modules.highlight.tokenArray = []; - // Scan text for new tokens - var text = context.fn.getContents(); - // Perform a scan for each module which provides any expressions to scan for - // FIXME: This traverses the entire string once for every regex. Investigate - // whether |-concatenating regexes then traversing once is faster. - for ( var module in context.modules ) { - if ( module in $.wikiEditor.modules && 'exp' in $.wikiEditor.modules[module] ) { - for ( var exp in $.wikiEditor.modules[module].exp ) { - // Prepare configuration - var regex = $.wikiEditor.modules[module].exp[exp].regex; - var label = $.wikiEditor.modules[module].exp[exp].label; - var markAfter = $.wikiEditor.modules[module].exp[exp].markAfter || false; - // Search for tokens - var offset = 0, left, right, match; - while ( ( match = text.substr( offset ).match( regex ) ) != null ) { - right = ( left = offset + match.index ) + match[0].length; - tokenArray[tokenArray.length] = { - 'offset': markAfter ? right : left, - 'label': label, - 'tokenStart': left, - 'match': match - }; - // Move to the right of this match - offset = right; + fn: { + /** + * Creates a highlight module within a wikiEditor + * + * @param config Configuration object to create module from + */ + create: function ( context, config ) { + context.modules.highlight.markersStr = ''; + }, + /** + * Scans text division for tokens + * + * @param division + */ + scan: function ( context, division ) { + var tokenArray, text, module, exp, + left, right, match; + /*jshint eqnull: true */ + + // Remove all existing tokens + tokenArray = context.modules.highlight.tokenArray = []; + // Scan text for new tokens + text = context.fn.getContents(); + // Perform a scan for each module which provides any expressions to scan for + // FIXME: This traverses the entire string once for every regex. Investigate + // whether |-concatenating regexes then traversing once is faster. + for ( module in context.modules ) { + if ( module in $.wikiEditor.modules && 'exp' in $.wikiEditor.modules[module] ) { + for ( exp in $.wikiEditor.modules[module].exp ) { + // Prepare configuration + var regex = $.wikiEditor.modules[module].exp[exp].regex; + var label = $.wikiEditor.modules[module].exp[exp].label; + var markAfter = $.wikiEditor.modules[module].exp[exp].markAfter || false; + // Search for tokens + var offset = 0; + while ( ( match = text.substr( offset ).match( regex ) ) != null ) { + right = ( left = offset + match.index ) + match[0].length; + tokenArray[tokenArray.length] = { + offset: markAfter ? right : left, + label: label, + tokenStart: left, + match: match + }; + // Move to the right of this match + offset = right; + } } } } - } - // Sort by start - tokenArray.sort( function( a, b ) { return a.tokenStart - b.tokenStart; } ); - // Let the world know, a scan just happened! - context.fn.trigger( 'scan' ); - }, - /** - * Marks up text with HTML - * - * @param division - * @param tokens - */ - // FIXME: What do division and tokens do? - // TODO: Document the scan() and mark() APIs somewhere - 'mark': function( context, division, tokens ) { - // Reset markers - var markers = []; + // Sort by start + tokenArray.sort( function ( a, b ) { + return a.tokenStart - b.tokenStart; + } ); + // Let the world know, a scan just happened! + context.fn.trigger( 'scan' ); + }, - // Recycle markers that will be skipped in this run - if ( context.modules.highlight.markers && division != '' ) { - for ( var i = 0; i < context.modules.highlight.markers.length; i++ ) { - if ( context.modules.highlight.markers[i].skipDivision == division ) { - markers.push( context.modules.highlight.markers[i] ); - } - } - } - context.modules.highlight.markers = markers; + /** + * Marks up text with HTML + * + * @param division + * @param tokens + */ + // FIXME: What do division and tokens do? + // TODO: Document the scan() and mark() APIs somewhere + mark: function ( context, division, tokens ) { + var i, subtracted, oldLength, j, o; - // Get all markers - context.fn.trigger( 'mark' ); - markers.sort( function( a, b ) { return a.start - b.start || a.end - b.end; } ); + // Reset markers + var markers = []; - // Serialize the markers array to a string and compare it with the one stored in the previous run - if they're - // equal, there's no markers to change - var markersStr = ''; - for ( var i = 0; i < markers.length; i++ ) { - markersStr += markers[i].start + ',' + markers[i].end + ',' + markers[i].type + ','; - } - if ( context.modules.highlight.markersStr == markersStr ) { - // No change, bail out - return; - } - context.modules.highlight.markersStr = markersStr; - - // Traverse the iframe DOM, inserting markers where they're needed - store visited markers here so we know which - // markers should be removed - var visited = [], v = 0; - for ( var i = 0; i < markers.length; i++ ) { - if ( typeof markers[i].skipDivision !== 'undefined' && ( division == markers[i].skipDivision ) ) { - continue; - } - - // We want to isolate each marker, so we may need to split textNodes if a marker starts or ends halfway one. - var start = markers[i].start; - var s = context.fn.getOffset( start ); - if ( !s ) { - // This shouldn't happen - continue; - } - var startNode = s.node; - - // Don't wrap leading BRs, produces undesirable results - // FIXME: It's also possible that the offset is a bit high because getOffset() has incremented .length to - // fake the newline caused by startNode being in a P. In this case, prevent the textnode splitting below - // from making startNode an empty textnode, IE barfs on that - while ( startNode.nodeName == 'BR' || s.offset == startNode.nodeValue.length ) { - start++; - s = context.fn.getOffset( start ); - startNode = s.node; - } - - // The next marker starts somewhere in this textNode or at this BR - if ( s.offset > 0 && s.node.nodeName == '#text' ) { - // Split off the prefix - this leaves the prefix in the current node and puts the rest in a new node - // which is our start node - var newStartNode = startNode.splitText( s.offset < s.node.nodeValue.length ? - s.offset : s.node.nodeValue.length - 1 - ); - var oldStartNode = startNode; - startNode = newStartNode; - // Update offset objects. We don't need purgeOffsets(), simply manipulating the existing offset objects - // will suffice - // FIXME: This manipulates context.offsets directly, which is ugly, but the performance improvement vs. - // purgeOffsets() is worth it - this code doesn't set lastTextNode to newStartNode for offset objects - // with lastTextNode == oldStartNode, but that doesn't really matter - var subtracted = s.offset; - var oldLength = s.length; - - var j, o; - // Update offset objects referring to oldStartNode - for ( j = start - subtracted; j < start; j++ ) { - if ( j in context.offsets ) { - o = context.offsets[j]; - o.node = oldStartNode; - o.length = subtracted; - } - } - // Update offset objects referring to newStartNode - for ( j = start; j < start - subtracted + oldLength; j++ ) { - if ( j in context.offsets ) { - o = context.offsets[j]; - o.node = newStartNode; - o.offset -= subtracted; - o.length -= subtracted; - o.lastTextNode = oldStartNode; + // Recycle markers that will be skipped in this run + if ( context.modules.highlight.markers && division !== '' ) { + for ( i = 0; i < context.modules.highlight.markers.length; i++ ) { + if ( context.modules.highlight.markers[i].skipDivision == division ) { + markers.push( context.modules.highlight.markers[i] ); } } } - var end = markers[i].end; - // To avoid ending up at the first char of the next node, we grab the offset for end - 1 and add one to the - // offset - var e = context.fn.getOffset( end - 1 ); - if ( !e ) { - // This shouldn't happen - continue; + context.modules.highlight.markers = markers; + + // Get all markers + context.fn.trigger( 'mark' ); + markers.sort( function ( a, b ) { + return a.start - b.start || a.end - b.end; + } ); + + // Serialize the markers array to a string and compare it with the one stored in the previous run - if they're + // equal, there's no markers to change + var markersStr = ''; + for ( i = 0; i < markers.length; i++ ) { + markersStr += markers[i].start + ',' + markers[i].end + ',' + markers[i].type + ','; } - var endNode = e.node; - if ( e.offset + 1 < e.length - 1 && endNode.nodeName == '#text' ) { - // Split off the suffix. This puts the suffix in a new node and leaves the rest in endNode - var oldEndNode = endNode; - var newEndNode = endNode.splitText( e.offset + 1 ); - // Update offset objects - var subtracted = e.offset + 1; - var oldLength = e.length; - var j, o; - // Update offset objects referring to oldEndNode - for ( j = end - subtracted; j < end; j++ ) { - if ( j in context.offsets ) { - o = context.offsets[j]; - o.node = oldEndNode; - o.length = subtracted; + if ( context.modules.highlight.markersStr == markersStr ) { + // No change, bail out + return; + } + context.modules.highlight.markersStr = markersStr; + + // Traverse the iframe DOM, inserting markers where they're needed - store visited markers here so we know which + // markers should be removed + var visited = [], v = 0; + for ( i = 0; i < markers.length; i++ ) { + if ( typeof markers[i].skipDivision !== 'undefined' && ( division == markers[i].skipDivision ) ) { + continue; + } + + // We want to isolate each marker, so we may need to split textNodes if a marker starts or ends halfway one. + var start = markers[i].start; + var s = context.fn.getOffset( start ); + if ( !s ) { + // This shouldn't happen + continue; + } + var startNode = s.node; + + // Don't wrap leading BRs, produces undesirable results + // FIXME: It's also possible that the offset is a bit high because getOffset() has incremented .length to + // fake the newline caused by startNode being in a P. In this case, prevent the textnode splitting below + // from making startNode an empty textnode, IE barfs on that + while ( startNode.nodeName === 'BR' || s.offset === startNode.nodeValue.length ) { + start++; + s = context.fn.getOffset( start ); + startNode = s.node; + } + + // The next marker starts somewhere in this textNode or at this BR + if ( s.offset > 0 && s.node.nodeName == '#text' ) { + // Split off the prefix - this leaves the prefix in the current node and puts the rest in a new node + // which is our start node + var newStartNode = startNode.splitText( s.offset < s.node.nodeValue.length ? + s.offset : s.node.nodeValue.length - 1 + ); + var oldStartNode = startNode; + startNode = newStartNode; + // Update offset objects. We don't need purgeOffsets(), simply manipulating the existing offset objects + // will suffice + // FIXME: This manipulates context.offsets directly, which is ugly, but the performance improvement vs. + // purgeOffsets() is worth it - this code doesn't set lastTextNode to newStartNode for offset objects + // with lastTextNode == oldStartNode, but that doesn't really matter + subtracted = s.offset; + oldLength = s.length; + + // Update offset objects referring to oldStartNode + for ( j = start - subtracted; j < start; j++ ) { + if ( j in context.offsets ) { + o = context.offsets[j]; + o.node = oldStartNode; + o.length = subtracted; + } + } + // Update offset objects referring to newStartNode + for ( j = start; j < start - subtracted + oldLength; j++ ) { + if ( j in context.offsets ) { + o = context.offsets[j]; + o.node = newStartNode; + o.offset -= subtracted; + o.length -= subtracted; + o.lastTextNode = oldStartNode; + } } } - // We have to insert this one, as it might not exist: we didn't call getOffset( end ) - context.offsets[end] = { - 'node': newEndNode, - 'offset': 0, - 'length': oldLength - subtracted, - 'lastTextNode': oldEndNode - }; - // Update offset objects referring to newEndNode - for ( j = end + 1; j < end - subtracted + oldLength; j++ ) { - if ( j in context.offsets ) { - o = context.offsets[j]; - o.node = newEndNode; - o.offset -= subtracted; - o.length -= subtracted; - o.lastTextNode = oldEndNode; + var end = markers[i].end; + // To avoid ending up at the first char of the next node, we grab the offset for end - 1 and add one to the + // offset + var e = context.fn.getOffset( end - 1 ); + if ( !e ) { + // This shouldn't happen + continue; + } + var endNode = e.node; + if ( e.offset + 1 < e.length - 1 && endNode.nodeName == '#text' ) { + // Split off the suffix. This puts the suffix in a new node and leaves the rest in endNode + var oldEndNode = endNode; + var newEndNode = endNode.splitText( e.offset + 1 ); + // Update offset objects + subtracted = e.offset + 1; + oldLength = e.length; + + // Update offset objects referring to oldEndNode + for ( j = end - subtracted; j < end; j++ ) { + if ( j in context.offsets ) { + o = context.offsets[j]; + o.node = oldEndNode; + o.length = subtracted; + } + } + // We have to insert this one, as it might not exist: we didn't call getOffset( end ) + context.offsets[end] = { + 'node': newEndNode, + 'offset': 0, + 'length': oldLength - subtracted, + 'lastTextNode': oldEndNode + }; + // Update offset objects referring to newEndNode + for ( j = end + 1; j < end - subtracted + oldLength; j++ ) { + if ( j in context.offsets ) { + o = context.offsets[j]; + o.node = newEndNode; + o.offset -= subtracted; + o.length -= subtracted; + o.lastTextNode = oldEndNode; + } } } - } - // Don't wrap trailing BRs, doing that causes weird issues - if ( endNode.nodeName == 'BR' ) { - endNode = e.lastTextNode; - } - // If startNode and endNode have different parents, we need to pull endNode and all textnodes in between - // into startNode's parent and replace

with
- if ( startNode.parentNode != endNode.parentNode ) { - var startP = $( startNode ).closest( 'p' ).get( 0 ); - var t = new context.fn.rawTraverser( startNode, startP, context.$content.get( 0 ), false ); - var afterStart = startNode.nextSibling; - var lastP = startP; - var nextT = t.next(); - while ( nextT && t.node != endNode ) { - t = nextT; - nextT = t.next(); - // If t.node has a different parent, merge t.node.parentNode with startNode.parentNode - if ( t.node.parentNode != startNode.parentNode ) { - var oldParent = t.node.parentNode; - if ( afterStart ) { - if ( lastP != t.inP ) { - // We're entering a new

, insert a
- startNode.parentNode.insertBefore( - startNode.ownerDocument.createElement( 'br' ), - afterStart - ); - } - // A

with just a
in it is an empty line, so let's not bother with unwrapping it - if ( !( oldParent.childNodes.length == 1 && oldParent.firstChild.nodeName == 'BR' ) ) { - // Move all children of oldParent into startNode's parent - while ( oldParent.firstChild ) { - startNode.parentNode.insertBefore( oldParent.firstChild, afterStart ); - } - } - } else { - if ( lastP != t.inP ) { - // We're entering a new

, insert a
- startNode.parentNode.appendChild( - startNode.ownerDocument.createElement( 'br' ) - ); - } - // A

with just a
in it is an empty line, so let's not bother with unwrapping it - if ( !( oldParent.childNodes.length == 1 && oldParent.firstChild.nodeName == 'BR' ) ) { - // Move all children of oldParent into startNode's parent - while ( oldParent.firstChild ) { - startNode.parentNode.appendChild( oldParent.firstChild ); + // Don't wrap trailing BRs, doing that causes weird issues + if ( endNode.nodeName == 'BR' ) { + endNode = e.lastTextNode; + } + // If startNode and endNode have different parents, we need to pull endNode and all textnodes in between + // into startNode's parent and replace

with
+ if ( startNode.parentNode !== endNode.parentNode ) { + var startP = $( startNode ).closest( 'p' ).get( 0 ); + var t = new context.fn.rawTraverser( startNode, startP, context.$content.get( 0 ), false ); + var afterStart = startNode.nextSibling; + var lastP = startP; + var nextT = t.next(); + while ( nextT && t.node !== endNode ) { + t = nextT; + nextT = t.next(); + // If t.node has a different parent, merge t.node.parentNode with startNode.parentNode + if ( t.node.parentNode !== startNode.parentNode ) { + var oldParent = t.node.parentNode; + if ( afterStart ) { + if ( lastP !== t.inP ) { + // We're entering a new

, insert a
+ startNode.parentNode.insertBefore( + startNode.ownerDocument.createElement( 'br' ), + afterStart + ); + } + // A

with just a
in it is an empty line, so let's not bother with unwrapping it + if ( !( oldParent.childNodes.length == 1 && oldParent.firstChild.nodeName == 'BR' ) ) { + // Move all children of oldParent into startNode's parent + while ( oldParent.firstChild ) { + startNode.parentNode.insertBefore( oldParent.firstChild, afterStart ); + } + } + } else { + if ( lastP !== t.inP ) { + // We're entering a new

, insert a
+ startNode.parentNode.appendChild( + startNode.ownerDocument.createElement( 'br' ) + ); + } + // A

with just a
in it is an empty line, so let's not bother with unwrapping it + if ( !( oldParent.childNodes.length == 1 && oldParent.firstChild.nodeName == 'BR' ) ) { + // Move all children of oldParent into startNode's parent + while ( oldParent.firstChild ) { + startNode.parentNode.appendChild( oldParent.firstChild ); + } } } + // Remove oldParent, which is now empty + oldParent.parentNode.removeChild( oldParent ); } - // Remove oldParent, which is now empty - oldParent.parentNode.removeChild( oldParent ); + lastP = t.inP; } - lastP = t.inP; + // Moving nodes around like this invalidates offset objects + // TODO: Update offset objects ourselves for performance. Requires rewriting this code block to be + // offset-based rather than traverser-based } - // Moving nodes around like this invalidates offset objects - // TODO: Update offset objects ourselves for performance. Requires rewriting this code block to be - // offset-based rather than traverser-based - } - // Now wrap everything between startNode and endNode (may be equal). - var ca1 = startNode, ca2 = endNode; - if ( ca1 && ca2 && ca1.parentNode ) { - var anchor = markers[i].getAnchor( ca1, ca2 ); - if ( !anchor ) { - var commonAncestor = ca1.parentNode; - if ( markers[i].anchor == 'wrap') { - // We have to store things like .parentNode and .nextSibling because appendChild() changes these - var newNode = ca1.ownerDocument.createElement( 'span' ); - var nextNode = ca2.nextSibling; - // Append all nodes between ca1 and ca2 (inclusive) to newNode - var n = ca1; - while ( n != nextNode ) { - var ns = n.nextSibling; - newNode.appendChild( n ); - n = ns; + // Now wrap everything between startNode and endNode (may be equal). + var ca1 = startNode, ca2 = endNode; + if ( ca1 && ca2 && ca1.parentNode ) { + var anchor = markers[i].getAnchor( ca1, ca2 ); + if ( !anchor ) { + var commonAncestor = ca1.parentNode; + if ( markers[i].anchor == 'wrap') { + // We have to store things like .parentNode and .nextSibling because appendChild() changes these + var newNode = ca1.ownerDocument.createElement( 'span' ); + var nextNode = ca2.nextSibling; + // Append all nodes between ca1 and ca2 (inclusive) to newNode + var n = ca1; + while ( n !== nextNode ) { + var ns = n.nextSibling; + newNode.appendChild( n ); + n = ns; + } + // Insert newNode in the right place + if ( nextNode ) { + commonAncestor.insertBefore( newNode, nextNode ); + } else { + commonAncestor.appendChild( newNode ); + } + anchor = newNode; + } else if ( markers[i].anchor == 'tag' ) { + anchor = commonAncestor; } - // Insert newNode in the right place - if ( nextNode ) { - commonAncestor.insertBefore( newNode, nextNode ); - } else { - commonAncestor.appendChild( newNode ); - } - anchor = newNode; - } else if ( markers[i].anchor == 'tag' ) { - anchor = commonAncestor; - } - $( anchor ).data( 'marker', markers[i] ).addClass( 'wikiEditor-highlight' ); - // Allow the module adding this marker to manipulate it - markers[i].afterWrap( anchor, markers[i] ); + $( anchor ).data( 'marker', markers[i] ).addClass( 'wikiEditor-highlight' ); + // Allow the module adding this marker to manipulate it + markers[i].afterWrap( anchor, markers[i] ); + } else { + // Update the marker object + $( anchor ).data( 'marker', markers[i] ); + if ( typeof markers[i].onSkip == 'function' ) { + markers[i].onSkip( anchor ); + } + } + visited[v++] = anchor; + } + } + // Remove markers that were previously inserted but weren't passed to this function - visited[] contains the + // visited elements in order and find() and each() preserve order + j = 0; + context.$content.find( '.wikiEditor-highlight' ).each( function () { + if ( visited[j] == this ) { + // This marker is legit, leave it in + j++; + return true; + } + // Remove this marker + var marker = $(this).data( 'marker' ); + if ( marker && typeof marker.skipDivision !== 'undefined' && ( division === marker.skipDivision ) ) { + // Don't remove these either + return true; + } + if ( marker && typeof marker.beforeUnwrap === 'function' ) + marker.beforeUnwrap( this ); + if ( ( marker && marker.anchor === 'tag' ) || $(this).is( 'p' ) ) { + // Remove all classes + $(this).removeAttr( 'class' ); } else { - // Update the marker object - $( anchor ).data( 'marker', markers[i] ); - if ( typeof markers[i].onSkip == 'function' ) { - markers[i].onSkip( anchor ); - } + // Assume anchor == 'wrap' + $(this).replaceWith( this.childNodes ); } - visited[v++] = anchor; - } + context.fn.purgeOffsets(); + }); + } - // Remove markers that were previously inserted but weren't passed to this function - visited[] contains the - // visited elements in order and find() and each() preserve order - var j = 0; - context.$content.find( '.wikiEditor-highlight' ).each( function() { - if ( visited[j] == this ) { - // This marker is legit, leave it in - j++; - return true; - } - // Remove this marker - var marker = $(this).data( 'marker' ); - if ( marker && typeof marker.skipDivision != 'undefined' && ( division == marker.skipDivision ) ) { - // Don't remove these either - return true; - } - if ( marker && typeof marker.beforeUnwrap == 'function' ) - marker.beforeUnwrap( this ); - if ( ( marker && marker.anchor == 'tag' ) || $(this).is( 'p' ) ) { - // Remove all classes - $(this).removeAttr( 'class' ); - } else { - // Assume anchor == 'wrap' - $(this).replaceWith( this.childNodes ); - } - context.fn.purgeOffsets(); - }); - } -} +}; -}; })( jQuery ); +}( jQuery ) ); diff --git a/modules/jquery.wikiEditor.js b/modules/jquery.wikiEditor.js index c2fb4094..6e87a45f 100644 --- a/modules/jquery.wikiEditor.js +++ b/modules/jquery.wikiEditor.js @@ -2,13 +2,13 @@ * This plugin provides a way to build a wiki-text editing user interface around a textarea. * * @example To intialize without any modules: - * $( 'div#edittoolbar' ).wikiEditor(); + * $( 'div#edittoolbar' ).wikiEditor(); * * @example To initialize with one or more modules, or to add modules after it's already been initialized: - * $( 'textarea#wpTextbox1' ).wikiEditor( 'addModule', 'toolbar', { ... config ... } ); + * $( 'textarea#wpTextbox1' ).wikiEditor( 'addModule', 'toolbar', { ... config ... } ); * */ -( function( $ ) { +( function ( $ ) { /** * Global static object for wikiEditor that provides generally useful functionality to all modules and contexts. @@ -19,63 +19,68 @@ $.wikiEditor = { * module name. The existance of a module in this object only indicates the module is available. To check if a * module is in use by a specific context check the context.modules object. */ - 'modules': {}, + modules: {}, + /** * A context can be extended, such as adding iframe support, on a per-wikiEditor instance basis. */ - 'extensions': {}, + extensions: {}, + /** * In some cases like with the iframe's HTML file, it's convienent to have a lookup table of all instances of the * WikiEditor. Each context contains an instance field which contains a key that corrosponds to a reference to the * textarea which the WikiEditor was build around. This way, by passing a simple integer you can provide a way back * to a specific context. */ - 'instances': [], + instances: [], + /** * For each browser name, an array of conditions that must be met are supplied in [operaton, value]-form where * operation is a string containing a JavaScript compatible binary operator and value is either a number to be * compared with $.browser.versionNumber or a string to be compared with $.browser.version. If a browser is not * specifically mentioned, we just assume things will work. */ - 'browsers': { + browsers: { // Left-to-right languages - 'ltr': { + ltr: { // The toolbar layout is broken in IE6 - 'msie': [['>=', 7]], + msie: [['>=', 7]], // Layout issues in FF < 2 - 'firefox': [['>=', 2]], + firefox: [['>=', 2]], // Text selection bugs galore - this may be a different situation with the new iframe-based solution - 'opera': [['>=', 9.6]], + opera: [['>=', 9.6]], // jQuery minimums - 'safari': [['>=', 3]], - 'chrome': [['>=', 3]], - 'netscape': [['>=', 9]], - 'blackberry': false, - 'ipod': false, - 'iphone': false + safari: [['>=', 3]], + chrome: [['>=', 3]], + netscape: [['>=', 9]], + blackberry: false, + ipod: false, + iphone: false }, // Right-to-left languages - 'rtl': { + rtl: { // The toolbar layout is broken in IE 7 in RTL mode, and IE6 in any mode - 'msie': [['>=', 8]], + msie: [['>=', 8]], // Layout issues in FF < 2 - 'firefox': [['>=', 2]], + firefox: [['>=', 2]], // Text selection bugs galore - this may be a different situation with the new iframe-based solution - 'opera': [['>=', 9.6]], + opera: [['>=', 9.6]], // jQuery minimums - 'safari': [['>=', 3]], - 'chrome': [['>=', 3]], - 'netscape': [['>=', 9]], - 'blackberry': false, - 'ipod': false, - 'iphone': false + safari: [['>=', 3]], + chrome: [['>=', 3]], + netscape: [['>=', 9]], + blackberry: false, + ipod: false, + iphone: false } }, + /** * Path to images - this is a bit messy, and it would need to change if this code (and images) gets moved into the * core - or anywhere for that matter... */ - 'imgPath' : mw.config.get( 'wgExtensionAssetsPath' ) + '/WikiEditor/modules/images/', + imgPath : mw.config.get( 'wgExtensionAssetsPath' ) + '/WikiEditor/modules/images/', + /** * Checks the current browser against the browsers object to determine if the browser has been black-listed or not. * Because these rules are often very complex, the object contains configurable operators and can check against @@ -88,7 +93,7 @@ $.wikiEditor = { * "open-web" way to go. * @param module Module object, defaults to $.wikiEditor */ - 'isSupported': function( module ) { + isSupported: function ( module ) { // Fallback to the wikiEditor browser map if no special map is provided in the module var mod = module && 'browsers' in module ? module : $.wikiEditor; // Check for and make use of cached value and early opportunities to bail @@ -99,21 +104,23 @@ $.wikiEditor = { // Run a browser support test and then cache and return the result return mod.supported = $.client.test( mod.browsers ); }, + /** * Checks if a module has a specific requirement * @param module Module object * @param requirement String identifying requirement */ - 'isRequired': function( module, requirement ) { - if ( typeof module['req'] !== 'undefined' ) { - for ( var req in module['req'] ) { - if ( module['req'][req] == requirement ) { + isRequired: function ( module, requirement ) { + if ( typeof module.req !== 'undefined' ) { + for ( var req in module.req ) { + if ( module.req[req] == requirement ) { return true; } } } return false; }, + /** * Provides a way to extract messages from objects. Wraps the mediaWiki.msg() function, which * may eventually become a wrapper for some kind of core MW functionality. @@ -124,7 +131,7 @@ $.wikiEditor = { * would return the raw text 'that', while passing property as 'foo' would return the internationalized message * with the key 'bar'. */ - 'autoMsg': function( object, property ) { + autoMsg: function ( object, property ) { // Accept array of possible properties, of which the first one found will be used if ( typeof property == 'object' ) { for ( var i in property ) { @@ -147,6 +154,7 @@ $.wikiEditor = { return ''; } }, + /** * Provides a way to extract a property of an object in a certain language, falling back on the property keyed as * 'default' or 'default-rtl'. If such key doesn't exist, the object itself is considered the actual value, which @@ -156,10 +164,11 @@ $.wikiEditor = { * @param object Object to extract property from * @param lang Language code, defaults to wgUserLanguage */ - 'autoLang': function( object, lang ) { + autoLang: function ( object, lang ) { var defaultKey = $( 'body' ).hasClass( 'rtl' ) ? 'default-rtl' : 'default'; return object[lang || mw.config.get( 'wgUserLanguage' )] || object[defaultKey] || object['default'] || object; }, + /** * Provides a way to extract the path of an icon in a certain language, automatically appending a version number for * caching purposes and prepending an image path when icon paths are relative. @@ -168,7 +177,7 @@ $.wikiEditor = { * @param path Default icon path, defaults to $.wikiEditor.imgPath * @param lang Language code, defaults to wgUserLanguage */ - 'autoIcon': function( icon, path, lang ) { + autoIcon: function ( icon, path, lang ) { var src = $.wikiEditor.autoLang( icon, lang ); path = path || $.wikiEditor.imgPath; // Prepend path if src is not absolute @@ -177,6 +186,7 @@ $.wikiEditor = { } return src + '?' + mw.loader.version( 'jquery.wikiEditor' ); }, + /** * Get the sprite offset for a language if available, icon for a language if available, or the default offset or icon, * in that order of preference. @@ -185,7 +195,7 @@ $.wikiEditor = { * @param path Icon path, see autoIcon() * @param lang Language code, defaults to wgUserLanguage */ - 'autoIconOrOffset': function( icon, offset, path, lang ) { + autoIconOrOffset: function ( icon, offset, path, lang ) { lang = lang || mw.config.get( 'wgUserLanguage' ); if ( typeof offset == 'object' && lang in offset ) { return offset[lang]; @@ -200,7 +210,7 @@ $.wikiEditor = { /** * jQuery plugin that provides a way to initialize a wikiEditor instance on a textarea. */ -$.fn.wikiEditor = function() { +$.fn.wikiEditor = function () { // Skip any further work when running in browsers that are unsupported if ( !$.wikiEditor.isSupported() ) { @@ -263,21 +273,22 @@ if ( !context || typeof context == 'undefined' ) { * @param data Either a string of the name of a module to add without any additional configuration parameters, * or an object with members keyed with module names and valued with configuration objects. */ - 'addModule': function( context, data ) { - var modules = {}; + 'addModule': function ( context, data ) { + var module, call, + modules = {}; if ( typeof data == 'string' ) { modules[data] = {}; } else if ( typeof data == 'object' ) { modules = data; } - for ( var module in modules ) { + for ( module in modules ) { // Check for the existance of an available / supported module with a matching name and a create function if ( typeof module == 'string' && typeof $.wikiEditor.modules[module] !== 'undefined' && $.wikiEditor.isSupported( $.wikiEditor.modules[module] ) ) { // Extend the context's core API with this module's own API calls if ( 'api' in $.wikiEditor.modules[module] ) { - for ( var call in $.wikiEditor.modules[module].api ) { + for ( call in $.wikiEditor.modules[module].api ) { // Modules may not overwrite existing API functions - first come, first serve if ( !( call in context.api ) ) { context.api[call] = $.wikiEditor.modules[module].api[call]; @@ -313,7 +324,7 @@ if ( !context || typeof context == 'undefined' ) { /** * Executes core event filters as well as event handlers provided by modules. */ - 'trigger': function( name, event ) { + trigger: function ( name, event ) { // Event is an optional argument, but from here on out, at least the type field should be dependable if ( typeof event == 'undefined' ) { event = { 'type': 'custom' }; @@ -339,9 +350,9 @@ if ( !context || typeof context == 'undefined' ) { name in $.wikiEditor.modules[module].evt ) { var ret = $.wikiEditor.modules[module].evt[name]( context, event ); - if (ret != null) { + if (ret !== null) { //if 1 returns false, the end result is false - if( returnFromModules == null ) { + if( returnFromModules === null ) { returnFromModules = ret; } else { returnFromModules = returnFromModules && ret; @@ -349,45 +360,47 @@ if ( !context || typeof context == 'undefined' ) { } } } - if ( returnFromModules != null ) { + if ( returnFromModules !== null ) { return returnFromModules; } else { return true; } }, + /** * Adds a button to the UI */ - 'addButton': function( options ) { + addButton: function ( options ) { // Ensure that buttons and tabs are visible context.$controls.show(); context.$buttons.show(); - return $( '