mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-29 00:30:44 +00:00
dda2c932bd
Created jQuery plugin MultiSuggest which builds a categorized dropdown under specified input box. Revised inspector to no longer be an iframe but to contain an Iframe. This reduces xbrowser issues with positioning and toggling inspector container. Added Inspector overlay element for positioning arbitrary elements over the iFrame. This prevents growing the iframe to arbitrary lenghts. Change-Id: I8efbbd091b0b24a19a4b73aa122d21a329cf97e4
324 lines
8.9 KiB
JavaScript
324 lines
8.9 KiB
JavaScript
/**
|
|
* VisualEditor user interface Context class.
|
|
*
|
|
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* Creates an ve.ui.Context object.
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @param {jQuery} $overlay DOM selection to add nodes to
|
|
*/
|
|
ve.ui.Context = function ( surfaceView, $overlay ) {
|
|
// Inheritance
|
|
if ( !surfaceView ) {
|
|
return;
|
|
}
|
|
|
|
// Properties
|
|
this.surfaceView = surfaceView;
|
|
this.inspectors = {};
|
|
this.inspector = null;
|
|
this.position = null;
|
|
this.clicking = false;
|
|
this.$ = $( '<div class="es-contextView"></div>' ).appendTo( $overlay || $( 'body' ) );
|
|
this.$toolbar = $( '<div class="es-contextView-toolbar"></div>' );
|
|
|
|
// Create iframe which will contain context inspectors.
|
|
this.setupInspectorFrame();
|
|
|
|
this.$icon = $( '<div class="es-contextView-icon"></div>' ).appendTo( this.$ );
|
|
this.toolbarView = new ve.ui.Toolbar(
|
|
this.$toolbar,
|
|
this.surfaceView,
|
|
[{ 'name': 'textStyle', 'items' : [ 'bold', 'italic', 'link', 'clear' ] }]
|
|
);
|
|
this.menuView = new ve.ui.Menu( [
|
|
// Example menu items
|
|
{ 'name': 'tools', '$': this.$toolbar }
|
|
],
|
|
null,
|
|
this.$
|
|
);
|
|
|
|
// Events
|
|
this.$icon.on( {
|
|
'mousedown': ve.bind( this.onMouseDown, this ),
|
|
'mouseup': ve.bind( this.onMouseUp, this )
|
|
} );
|
|
this.surfaceView.getDocument().getDocumentNode().$.on( {
|
|
'focus': ve.bind( this.onDocumentFocus, this ),
|
|
'blur': ve.bind( this.onDocumentBlur, this )
|
|
} );
|
|
|
|
// Intitialize link inspector
|
|
this.addInspector( 'link', new ve.ui.LinkInspector( this.toolbarView, this ) );
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
ve.ui.Context.prototype.setupInspectorFrame = function () {
|
|
var $styleLink;
|
|
|
|
// Inspector container
|
|
this.$inspectors = $( '<div />' )
|
|
.addClass( 'es-contextView-inspectors' )
|
|
.appendTo( this.$ );
|
|
|
|
// Iframe overlay
|
|
this.$iframeOverlay = $('<div />')
|
|
.addClass( 've-iframe-overlay' )
|
|
.appendTo( this.$inspectors );
|
|
|
|
// Create and append an iframe for inspectors.
|
|
// Use of iframe is required to retain selection while inspector controls are focused.
|
|
this.$inspectorFrame = $( '<iframe></iframe>' )
|
|
.attr({
|
|
'frameborder': '0'
|
|
})
|
|
.appendTo( this.$inspectors );
|
|
|
|
// Stash iframe document reference to properly create & append elements.
|
|
this.inspectorDoc = this.$inspectorFrame.prop( 'contentWindow' ).document;
|
|
|
|
// Cross browser trick to append content to an iframe
|
|
// Write a containing element to the iframe
|
|
this.inspectorDoc.write( '<div class="ve-iframe-wrapper"></div>' );
|
|
this.inspectorDoc.close();
|
|
this.$inspectorWrapper = $( this.inspectorDoc ).find( '.ve-iframe-wrapper' );
|
|
|
|
// Create style element in iframe document scope
|
|
$styleLink =
|
|
$( '<link>', this.inspectorDoc )
|
|
.attr( {
|
|
'rel': 'stylesheet',
|
|
'type': 'text/css',
|
|
'media': 'screen',
|
|
'href': ve.init.platform.getModulesUrl() + '/ve/ui/styles/ve.ui.Inspector.css'
|
|
} );
|
|
|
|
// Append inspector styles to iframe head
|
|
$( this.inspectorDoc ).find( 'head' ).append( $styleLink );
|
|
|
|
// Adjust iframe body styles.
|
|
$( 'body', this.inspectorDoc ).css( {
|
|
'padding': '0px 5px',
|
|
'margin': 0
|
|
} );
|
|
};
|
|
|
|
ve.ui.Context.prototype.onDocumentFocus = function () {
|
|
$( window ).on( 'resize.ve-ui-context scroll.ve-ui-context', ve.bind( this.set, this ) );
|
|
};
|
|
|
|
ve.ui.Context.prototype.onDocumentBlur = function () {
|
|
$( window ).off( 'resize.ve-ui-context scroll.ve-ui-context' );
|
|
};
|
|
|
|
ve.ui.Context.prototype.onMouseDown = function ( e ) {
|
|
this.clicking = true;
|
|
e.preventDefault();
|
|
return false;
|
|
};
|
|
|
|
ve.ui.Context.prototype.onMouseUp = function ( e ) {
|
|
if ( this.clicking && e.which === 1 ) {
|
|
if ( this.inspector ) {
|
|
this.closeInspector();
|
|
} else {
|
|
if ( this.isMenuOpen() ) {
|
|
this.closeMenu();
|
|
} else {
|
|
this.openMenu();
|
|
}
|
|
}
|
|
}
|
|
this.clicking = false;
|
|
};
|
|
|
|
ve.ui.Context.prototype.getSurfaceView = function () {
|
|
return this.surfaceView;
|
|
};
|
|
|
|
ve.ui.Context.prototype.openMenu = function () {
|
|
this.menuView.open();
|
|
};
|
|
|
|
ve.ui.Context.prototype.closeMenu = function () {
|
|
this.menuView.close();
|
|
};
|
|
|
|
ve.ui.Context.prototype.isMenuOpen = function () {
|
|
return this.menuView.isOpen();
|
|
};
|
|
|
|
ve.ui.Context.prototype.areChildrenCurrentlyVisible = function () {
|
|
return this.inspector !== null || this.menuView.isOpen();
|
|
};
|
|
|
|
ve.ui.Context.prototype.set = function () {
|
|
if ( this.surfaceView.getModel().getSelection().getLength() > 0 ) {
|
|
this.positionIcon();
|
|
if ( this.position ) {
|
|
this.positionOverlay( this.menuView.$ );
|
|
if ( this.inspector ) {
|
|
this.positionOverlay ( this.$inspectors );
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
ve.ui.Context.prototype.positionIcon = function () {
|
|
this.$.removeClass( 'es-contextView-position-start es-contextView-position-end' );
|
|
|
|
var selection = this.surfaceView.model.getSelection(),
|
|
selectionRect = this.surfaceView.getSelectionRect();
|
|
|
|
if ( selection.to > selection.from ) {
|
|
this.position = new ve.Position( selectionRect.end.x, selectionRect.end.y );
|
|
this.$.addClass( 'es-contextView-position-end' );
|
|
} else {
|
|
this.position = new ve.Position( selectionRect.start.x, selectionRect.start.y );
|
|
this.$.addClass( 'es-contextView-position-start' );
|
|
}
|
|
|
|
this.$.css( {
|
|
'left': this.position.left,
|
|
'top': this.position.top
|
|
} );
|
|
this.$icon.fadeIn( 'fast' );
|
|
};
|
|
|
|
ve.ui.Context.prototype.positionOverlay = function ( $overlay ) {
|
|
var overlayMargin = 5,
|
|
overlayWidth = $overlay.outerWidth(),
|
|
overlayHeight = $overlay.outerHeight(),
|
|
$window = $( window ),
|
|
windowWidth = $window.width(),
|
|
windowHeight = $window.height(),
|
|
windowScrollTop = $window.scrollTop(),
|
|
selection = this.surfaceView.model.getSelection(),
|
|
// Center align overlay
|
|
overlayLeft = -Math.round( overlayWidth / 2 );
|
|
|
|
// Adjust overlay left or right depending on viewport
|
|
if ( ( this.position.left - overlayMargin ) + overlayLeft < 0 ) {
|
|
// Move right a bit past center
|
|
overlayLeft -= this.position.left + overlayLeft - overlayMargin;
|
|
} else if ( ( overlayMargin + this.position.left ) - overlayLeft > windowWidth ) {
|
|
// Move left a bit past center
|
|
overlayLeft += windowWidth - overlayMargin - ( this.position.left - overlayLeft );
|
|
}
|
|
|
|
$overlay.css( 'left', overlayLeft );
|
|
|
|
// Position overlay on top or bottom depending on viewport
|
|
this.$.removeClass( 'es-contextView-position-below es-contextView-position-above' );
|
|
if (
|
|
selection.from < selection.to &&
|
|
this.position.top + overlayHeight + ( overlayMargin * 2 ) < windowHeight + windowScrollTop
|
|
) {
|
|
this.$.addClass( 'es-contextView-position-below' );
|
|
} else {
|
|
this.$.addClass( 'es-contextView-position-above' );
|
|
}
|
|
|
|
};
|
|
|
|
// Method to position iframe overlay above or below an element.
|
|
ve.ui.Context.prototype.positionIframeOverlay = function( config ) {
|
|
var left, top;
|
|
if (
|
|
config === undefined ||
|
|
! ( 'overlay' in config )
|
|
) {
|
|
return;
|
|
}
|
|
// Set iframe overlay below element.
|
|
if ( 'below' in config ) {
|
|
left = config.below.offset().left;
|
|
top = config.below.offset().top + config.below.outerHeight();
|
|
// Set iframe overlay above element.
|
|
} else if ( 'above' in config ) {
|
|
left = config.above.offset().left;
|
|
top = config.above.offset().top;
|
|
}
|
|
// Set position.
|
|
config.overlay.css( {
|
|
'left': left,
|
|
'top': top
|
|
} );
|
|
};
|
|
|
|
ve.ui.Context.prototype.clear = function () {
|
|
if ( this.inspector ) {
|
|
this.closeInspector();
|
|
}
|
|
this.$icon.hide();
|
|
this.menuView.close();
|
|
};
|
|
|
|
ve.ui.Context.prototype.openInspector = function ( name ) {
|
|
if ( !( name in this.inspectors ) ) {
|
|
throw new Error( 'Missing inspector error. Can not open nonexistent inspector: ' + name );
|
|
}
|
|
this.inspectors[name].open();
|
|
this.resizeInspectorFrame( this.inspectors[name] );
|
|
// Setting this to auto makes position overlay work correctly.
|
|
this.$inspectors.css({
|
|
'height': 'auto',
|
|
'width': 'auto',
|
|
'visibility': 'visible'
|
|
});
|
|
this.positionOverlay( this.$inspectors );
|
|
this.inspector = name;
|
|
};
|
|
|
|
ve.ui.Context.prototype.closeInspector = function ( accept ) {
|
|
if ( this.inspector ) {
|
|
this.inspectors[this.inspector].close( accept );
|
|
this.inspector = null;
|
|
}
|
|
this.$inspectors.css({
|
|
'visibility': 'hidden'
|
|
});
|
|
};
|
|
|
|
ve.ui.Context.prototype.getInspector = function ( name ) {
|
|
if ( name in this.inspectors ) {
|
|
return this.inspectors[name];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
ve.ui.Context.prototype.addInspector = function ( name, inspector ) {
|
|
if ( name in this.inspectors ) {
|
|
throw new Error( 'Duplicate inspector error. Previous registration with the same name: ' + name );
|
|
}
|
|
inspector.$.hide();
|
|
this.inspectors[name] = inspector;
|
|
this.$inspectorWrapper.append( inspector.$ );
|
|
};
|
|
|
|
//TODO: need better iframe resizing. Currently sizes to dimensions of specified inspector.
|
|
ve.ui.Context.prototype.resizeInspectorFrame = function ( inspector ) {
|
|
var width = inspector.$.outerWidth(),
|
|
height = inspector.$.outerHeight();
|
|
this.$inspectorFrame.css( {
|
|
'width': width + 10,
|
|
'height': height + 10
|
|
} );
|
|
};
|
|
|
|
ve.ui.Context.prototype.removeInspector = function ( name ) {
|
|
if ( name in this.inspectors ) {
|
|
throw new Error( 'Missing inspector error. Can not remove nonexistent inspector: ' + name );
|
|
}
|
|
this.inspectors[name].detach();
|
|
delete this.inspectors[name];
|
|
this.inspector = null;
|
|
};
|