mediawiki-extensions-Visual.../modules/ve/ui/ve.ui.Context.js
Rob Moen dda2c932bd Added MW page suggestion functionality.
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
2012-08-17 14:12:26 -07:00

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;
};