%PDF- %PDF-
Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/ce/nodes/ |
Current File : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/ce/nodes/ve.ce.TableNode.js |
/*! * VisualEditor ContentEditable TableNode class. * * @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org */ /** * ContentEditable table node. * * @class * @extends ve.ce.BranchNode * @constructor * @param {ve.dm.TableNode} model Model to observe * @param {Object} [config] Configuration options */ ve.ce.TableNode = function VeCeTableNode() { // Parent constructor ve.ce.TableNode.super.apply( this, arguments ); // Properties this.surface = null; this.active = false; this.startCell = null; // Stores the original table selection as // a fragment when entering cell edit mode this.editingFragment = null; // DOM changes this.$element .addClass( 've-ce-tableNode' ) .prop( 'contentEditable', 'false' ); }; /* Inheritance */ OO.inheritClass( ve.ce.TableNode, ve.ce.BranchNode ); /* Methods */ /** * @inheritdoc */ ve.ce.TableNode.prototype.onSetup = function () { // Parent method ve.ce.TableNode.super.prototype.onSetup.call( this ); // Exit if already setup or not attached if ( this.isSetup || !this.root ) { return; } this.surface = this.getRoot().getSurface(); // Overlay this.$selectionBox = $( '<div>' ).addClass( 've-ce-tableNodeOverlay-selection-box' ); this.$selectionBoxAnchor = $( '<div>' ).addClass( 've-ce-tableNodeOverlay-selection-box-anchor' ); this.colContext = new ve.ui.TableLineContext( this, 'col' ); this.rowContext = new ve.ui.TableLineContext( this, 'row' ); this.$overlay = $( '<div>' ) .addClass( 've-ce-tableNodeOverlay oo-ui-element-hidden' ) .append( [ this.$selectionBox, this.$selectionBoxAnchor, this.colContext.$element, this.rowContext.$element, this.$rowBracket, this.$colBracket ] ); this.surface.surface.$blockers.append( this.$overlay ); // Events this.$element.on( { 'mousedown.ve-ce-tableNode': this.onTableMouseDown.bind( this ), 'dblclick.ve-ce-tableNode': this.onTableDblClick.bind( this ) } ); this.$overlay.on( { 'mousedown.ve-ce-tableNode': this.onTableMouseDown.bind( this ), 'dblclick.ve-ce-tableNode': this.onTableDblClick.bind( this ) } ); this.onTableMouseUpHandler = this.onTableMouseUp.bind( this ); this.onTableMouseMoveHandler = this.onTableMouseMove.bind( this ); // Select and position events both fire updateOverlay, so debounce. Also makes // sure that this.selectedRectangle is up to date before redrawing. this.updateOverlayDebounced = ve.debounce( this.updateOverlay.bind( this ) ); this.surface.getModel().connect( this, { select: 'onSurfaceModelSelect' } ); this.surface.connect( this, { position: this.updateOverlayDebounced } ); }; /** * @inheritdoc */ ve.ce.TableNode.prototype.onTeardown = function () { // Parent method ve.ce.TableNode.super.prototype.onTeardown.call( this ); // Events this.$element.off( '.ve-ce-tableNode' ); this.$overlay.off( '.ve-ce-tableNode' ); this.surface.getModel().disconnect( this ); this.surface.disconnect( this ); this.$overlay.remove(); }; /** * Handle table double click events * * @param {jQuery.Event} e Double click event */ ve.ce.TableNode.prototype.onTableDblClick = function ( e ) { var offset; if ( !this.getCellNodeFromEvent( e ) ) { return; } if ( this.surface.getModel().getSelection() instanceof ve.dm.TableSelection ) { // Don't change selection in setEditing to avoid scrolling to bottom of cell this.setEditing( true, true ); // getOffsetFromEventCoords doesn't work in ce=false in Firefox, so ensure // this is called after setEditing( true ). offset = this.surface.getOffsetFromEventCoords( e.originalEvent ); if ( offset !== -1 ) { // Set selection to where the double click happened this.surface.getModel().setLinearSelection( new ve.Range( offset ) ); } else { this.setEditing( true ); } } }; /** * Handle mouse down or touch start events * * @param {jQuery.Event} e Mouse down or touch start event */ ve.ce.TableNode.prototype.onTableMouseDown = function ( e ) { var cellNode, startCell, endCell, selection, newSelection, node = this; cellNode = this.getCellNodeFromEvent( e ); if ( !cellNode ) { return; } // Right-click if ( e.which === OO.ui.MouseButtons.RIGHT ) { // Select the cell to the browser renders the correct context menu ve.selectElement( cellNode.$element[ 0 ] ); setTimeout( function () { // Trigger onModelSelect to restore the selection node.surface.onModelSelect(); } ); return; } endCell = this.getModel().getMatrix().lookupCell( cellNode.getModel() ); if ( !endCell ) { e.preventDefault(); return; } selection = this.surface.getModel().getSelection(); startCell = e.shiftKey && this.active ? { col: selection.fromCol, row: selection.fromRow } : endCell; newSelection = new ve.dm.TableSelection( this.getModel().getDocument(), this.getModel().getOuterRange(), startCell.col, startCell.row, endCell.col, endCell.row, true ); if ( this.editingFragment ) { if ( newSelection.equals( this.editingFragment.getSelection() ) ) { // Clicking on the editing cell, don't prevent default return; } else { this.setEditing( false, true ); } } this.surface.getModel().setSelection( newSelection ); this.startCell = startCell; this.surface.$document.on( { 'mouseup touchend': this.onTableMouseUpHandler, 'mousemove touchmove': this.onTableMouseMoveHandler } ); e.preventDefault(); }; /** * Get a table cell node from a mouse event * * Works around various issues with touch events and browser support. * * @param {jQuery.Event} e Mouse event * @return {ve.ce.TableCellNode|null} Table cell node */ ve.ce.TableNode.prototype.getCellNodeFromEvent = function ( e ) { var touch, cellNode; // 'touchmove' doesn't give a correct e.target, so calculate it from coordinates if ( e.type === 'touchstart' && e.originalEvent.touches.length > 1 ) { // Ignore multi-touch return null; } else if ( e.type === 'touchmove' ) { if ( e.originalEvent.touches.length > 1 ) { // Ignore multi-touch return null; } touch = e.originalEvent.touches[ 0 ]; return this.getCellNodeFromPoint( touch.clientX, touch.clientY ); } else if ( OO.ui.contains( this.$overlay[ 0 ], e.target, true ) ) { // Support: IE<=10 // Browsers which don't support pointer-events:none will still fire events // on the overlay. Hide the overlay and get the target from the event coords. this.$overlay.addClass( 'oo-ui-element-hidden' ); cellNode = this.getCellNodeFromPoint( e.clientX, e.clientY ); this.$overlay.removeClass( 'oo-ui-element-hidden' ); return cellNode; } else { return this.getNearestCellNode( e.target ); } }; /** * Get the cell node from a point * * @param {number} x X offset * @param {number} y Y offset * @return {ve.ce.TableCellNode|null} Table cell node, or null if none found */ ve.ce.TableNode.prototype.getCellNodeFromPoint = function ( x, y ) { return this.getNearestCellNode( this.surface.getElementDocument().elementFromPoint( x, y ) ); }; /** * Get the nearest cell node in this table to an element * * If the nearest cell node is in another table, return null. * * @param {HTMLElement} element Element target to find nearest cell node to * @return {ve.ce.TableCellNode|null} Table cell node, or null if none found */ ve.ce.TableNode.prototype.getNearestCellNode = function ( element ) { var $element = $( element ), $table = $element.closest( 'table' ); // Nested table, ignore if ( !this.$element.is( $table ) ) { return null; } return $element.closest( 'td, th' ).data( 'view' ); }; /** * Handle mouse/touch move events * * @param {jQuery.Event} e Mouse/touch move event */ ve.ce.TableNode.prototype.onTableMouseMove = function ( e ) { var cell, selection, cellNode; cellNode = this.getCellNodeFromEvent( e ); if ( !cellNode ) { return; } cell = this.getModel().matrix.lookupCell( cellNode.getModel() ); if ( !cell ) { return; } selection = new ve.dm.TableSelection( this.getModel().getDocument(), this.getModel().getOuterRange(), this.startCell.col, this.startCell.row, cell.col, cell.row, true ); this.surface.getModel().setSelection( selection ); }; /** * Handle mouse up or touch end events * * @param {jQuery.Event} e Mouse up or touch end event */ ve.ce.TableNode.prototype.onTableMouseUp = function () { this.startCell = null; this.surface.$document.off( { 'mouseup touchend': this.onTableMouseUpHandler, 'mousemove touchmove': this.onTableMouseMoveHandler } ); }; /** * Set the editing state of the table * * @param {boolean} isEditing The table is being edited * @param {boolean} noSelect Don't change the selection */ ve.ce.TableNode.prototype.setEditing = function ( isEditing, noSelect ) { var cell, offset, cellRange, profile, activeCellNode, surfaceModel = this.surface.getModel(), selection = surfaceModel.getSelection(); if ( isEditing ) { if ( !selection.isSingleCell() ) { selection = selection.collapseToFrom(); this.surface.getModel().setSelection( selection ); } cell = this.getCellNodesFromSelection( selection )[ 0 ]; if ( !cell.isCellEditable() ) { return; } this.editingFragment = this.surface.getModel().getFragment( selection ); cell.setEditing( true ); if ( !noSelect ) { cellRange = cell.getModel().getRange(); offset = surfaceModel.getDocument().data.getNearestContentOffset( cellRange.end, -1 ); if ( offset > cellRange.start ) { surfaceModel.setLinearSelection( new ve.Range( offset ) ); } } } else if ( ( activeCellNode = this.getActiveCellNode() ) ) { activeCellNode.setEditing( false ); if ( !noSelect ) { surfaceModel.setSelection( this.editingFragment.getSelection() ); } this.editingFragment = null; } this.$element.toggleClass( 've-ce-tableNode-editing', isEditing ); // Support: Firefox 39 // HACK T103035: Firefox 39 has a regression in which clicking on a ce=false table // always selects the entire table, even if you click in a ce=true child. // Making the table ce=true does allow the user to make selections across cells // and corrupt the table in some circumstance, so restrict this hack as much // as possible. profile = $.client.profile(); if ( profile.layout === 'gecko' && profile.versionBase === '39' ) { this.$element.prop( 'contentEditable', isEditing.toString() ); } this.$overlay.toggleClass( 've-ce-tableNodeOverlay-editing', isEditing ); // Support: IE<=10 // If the browser doesn't support pointer-events:none, hide the selection boxes. if ( !this.surface.supportsPointerEvents() ) { this.$selectionBox.toggleClass( 'oo-ui-element-hidden', isEditing ); this.$selectionBoxAnchor.toggleClass( 'oo-ui-element-hidden', isEditing ); } }; /** * Handle select events from the surface model. * * @param {ve.dm.Selection} selection Selection */ ve.ce.TableNode.prototype.onSurfaceModelSelect = function ( selection ) { // The table is active if there is a linear selection inside a cell being edited // or a table selection matching this table. var active = ( this.editingFragment !== null && selection instanceof ve.dm.LinearSelection && this.editingFragment.getSelection().getRanges()[ 0 ].containsRange( selection.getRange() ) ) || ( selection instanceof ve.dm.TableSelection && selection.tableRange.equalsSelection( this.getModel().getOuterRange() ) ); if ( active ) { if ( !this.active ) { this.$overlay.removeClass( 'oo-ui-element-hidden' ); // Only register touchstart event after table has become active to prevent // accidental focusing of the table while scrolling this.$element.on( 'touchstart.ve-ce-tableNode', this.onTableMouseDown.bind( this ) ); } // Ignore update the overlay if the table selection changed, i.e. not an in-cell selection change if ( selection instanceof ve.dm.TableSelection ) { this.updateOverlayDebounced( true ); } } else if ( !active && this.active ) { this.$overlay.addClass( 'oo-ui-element-hidden' ); if ( this.editingFragment ) { this.setEditing( false, true ); } // When the table of the active node is deactivated, clear the active node if ( this.getActiveCellNode() ) { this.surface.setActiveNode( null ); } this.$element.off( 'touchstart.ve-ce-tableNode' ); } this.$element.toggleClass( 've-ce-tableNode-active', active ); this.active = active; }; /** * Get the active node in this table, if it has one * * @return {ve.ce.TableNode|null} The active cell node in this table */ ve.ce.TableNode.prototype.getActiveCellNode = function () { var activeNode = this.surface.getActiveNode(), tableNodeOfActiveCellNode = activeNode && activeNode instanceof ve.ce.TableCellNode && activeNode.findParent( ve.ce.TableNode ); return tableNodeOfActiveCellNode === this ? activeNode : null; }; /** * Update the overlay positions * * @param {boolean} selectionChanged The update was triggered by a selection change */ ve.ce.TableNode.prototype.updateOverlay = function ( selectionChanged ) { var i, l, anchorNode, anchorOffset, selectionOffset, selection, selectionRect, tableOffset, surfaceOffset, cells, editable = true; if ( !this.active || !this.root ) { return; } selection = this.editingFragment ? this.editingFragment.getSelection() : this.surface.getModel().getSelection(); // getBoundingClientRect is more accurate but must be used consistently // due to the iOS7 bug where it is relative to the document. tableOffset = this.getFirstSectionNode().$element[ 0 ].getBoundingClientRect(); surfaceOffset = this.surface.getSurface().$element[ 0 ].getBoundingClientRect(); if ( !tableOffset ) { return; } selectionRect = this.surface.getSelection( selection ).getSelectionBoundingRect(); if ( !selectionRect ) { return; } cells = selection.getMatrixCells(); anchorNode = this.getCellNodesFromSelection( selection.collapseToFrom() )[ 0 ]; anchorOffset = ve.translateRect( anchorNode.$element[ 0 ].getBoundingClientRect(), -tableOffset.left, -tableOffset.top ); // Compute a bounding box for the given cell elements for ( i = 0, l = cells.length; i < l; i++ ) { if ( editable && !cells[ i ].node.isCellEditable() ) { editable = false; } } selectionOffset = ve.translateRect( selectionRect, surfaceOffset.left - tableOffset.left, surfaceOffset.top - tableOffset.top ); // Resize controls this.$selectionBox.css( { top: selectionOffset.top, left: selectionOffset.left, width: selectionOffset.width, height: selectionOffset.height } ); this.$selectionBoxAnchor.css( { top: anchorOffset.top, left: anchorOffset.left, width: anchorOffset.width, height: anchorOffset.height } ); // Position controls this.$overlay.css( { top: tableOffset.top - surfaceOffset.top, left: tableOffset.left - surfaceOffset.left, width: tableOffset.width } ); this.colContext.$element.css( { left: selectionOffset.left } ); this.colContext.indicator.$element.css( { width: selectionOffset.width } ); this.colContext.popup.$element.css( { 'margin-left': selectionOffset.width / 2 } ); this.rowContext.$element.css( { top: selectionOffset.top } ); this.rowContext.indicator.$element.css( { height: selectionOffset.height } ); this.rowContext.popup.$element.css( { 'margin-top': selectionOffset.height / 2 } ); // Classes this.$selectionBox .toggleClass( 've-ce-tableNodeOverlay-selection-box-fullRow', selection.isFullRow() ) .toggleClass( 've-ce-tableNodeOverlay-selection-box-fullCol', selection.isFullCol() ) .toggleClass( 've-ce-tableNodeOverlay-selection-box-notEditable', !editable ); if ( selectionChanged ) { ve.scrollIntoView( this.$selectionBox.get( 0 ) ); } }; /** * Get the first section node of the table, skipping over any caption nodes * * @return {ve.ce.TableSectionNode} First table section node */ ve.ce.TableNode.prototype.getFirstSectionNode = function () { var i = 0; while ( !( this.children[ i ] instanceof ve.ce.TableSectionNode ) ) { i++; } return this.children[ i ]; }; /** * Get a cell node from a single cell selection * * @param {ve.dm.TableSelection} selection Single cell table selection * @return {ve.ce.TableCellNode[]} Cell nodes */ ve.ce.TableNode.prototype.getCellNodesFromSelection = function ( selection ) { var i, l, cellModel, cellView, cells = selection.getMatrixCells(), nodes = []; for ( i = 0, l = cells.length; i < l; i++ ) { cellModel = cells[ i ].node; cellView = this.getNodeFromOffset( cellModel.getOffset() - this.model.getOffset() ); nodes.push( cellView ); } return nodes; }; /* Static Properties */ ve.ce.TableNode.static.name = 'table'; ve.ce.TableNode.static.tagName = 'table'; /* Registration */ ve.ce.nodeFactory.register( ve.ce.TableNode );