| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module restricted-editing/restrictededitingediting
- */
- import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
- import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
- /**
- * @extends module:core/plugin~Plugin
- */
- export default class RestrictedEditingEditing extends Plugin {
- /**
- * @inheritDoc
- */
- static get pluginName() {
- return 'RestrictedEditingEditing';
- }
- /**
- * @inheritDoc
- */
- constructor( editor ) {
- super( editor );
- this._alwaysEnabled = new Set( [ 'undo', 'redo' ] );
- this._allowedInException = new Set( [ 'bold', 'italic', 'link', 'input', 'delete', 'forwardDelete' ] );
- }
- /**
- * @inheritDoc
- */
- init() {
- const editor = this.editor;
- let createdMarkers = 0;
- editor.conversion.for( 'upcast' ).add( upcastHighlightToMarker( {
- view: {
- name: 'span',
- classes: 'ck-restricted-editing-exception'
- },
- model: () => {
- createdMarkers++;
- return `restricted-editing-exception:${ createdMarkers }`;
- }
- } ) );
- editor.conversion.for( 'downcast' ).markerToHighlight( {
- model: 'restricted-editing-exception',
- // Use callback to return new object every time new marker instance is created - otherwise it will be seen as the same marker.
- view: () => ( {
- name: 'span',
- classes: 'ck-restricted-editing-exception',
- priority: -10
- } )
- } );
- this._disableCommands( editor );
- const selection = editor.model.document.selection;
- this.listenTo( selection, 'change', this._checkCommands.bind( this ) );
- this.listenTo( editor.model.document, 'change:data', this._checkCommands.bind( this ) );
- editor.model.document.registerPostFixer( writer => {
- let changeApplied = false;
- for ( const change of editor.model.document.differ.getChanges() ) {
- if ( change.type == 'insert' && change.name == '$text' && change.length === 1 ) {
- changeApplied = this._tryExtendMarkedEnd( change, writer, changeApplied ) || changeApplied;
- changeApplied = this._tryExtendMarkerStart( change, writer, changeApplied ) || changeApplied;
- }
- }
- return changeApplied;
- } );
- }
- _tryExtendMarkerStart( change, writer, changeApplied ) {
- const markerAtStart = this._getMarker( change.position.getShiftedBy( 1 ) );
- if ( markerAtStart && markerAtStart.getStart().isEqual( change.position.getShiftedBy( 1 ) ) ) {
- writer.updateMarker( markerAtStart, {
- range: writer.createRange( markerAtStart.getStart().getShiftedBy( -1 ), markerAtStart.getEnd() )
- } );
- changeApplied = true;
- }
- return changeApplied;
- }
- _tryExtendMarkedEnd( change, writer, changeApplied ) {
- const markerAtEnd = this._getMarker( change.position );
- if ( markerAtEnd && markerAtEnd.getEnd().isEqual( change.position ) ) {
- writer.updateMarker( markerAtEnd, {
- range: writer.createRange( markerAtEnd.getStart(), markerAtEnd.getEnd().getShiftedBy( 1 ) )
- } );
- changeApplied = true;
- }
- return changeApplied;
- }
- _checkCommands() {
- const editor = this.editor;
- const selection = editor.model.document.selection;
- if ( selection.rangeCount > 1 ) {
- this._disableCommands( editor );
- return;
- }
- const marker = this._getMarker( selection.focus );
- if ( isSelectionInExceptionMarker( marker, selection ) ) {
- this._enableCommands( marker );
- } else {
- this._disableCommands();
- }
- }
- _getMarker( position ) {
- const editor = this.editor;
- for ( const marker of editor.model.markers ) {
- const markerRange = marker.getRange();
- if ( isPositionInRangeOrOnRangeBoundary( markerRange, position ) ) {
- if ( marker.name.startsWith( 'restricted-editing-exception:' ) ) {
- return marker;
- }
- }
- }
- }
- _enableCommands( marker ) {
- const editor = this.editor;
- const exceptionDisable = [];
- const commands = this._getCommandNamesToToggle( editor, this._allowedInException )
- .filter( name => this._allowedInException.has( name ) )
- .filter( name => {
- const selection = editor.model.document.selection;
- const markerRange = marker.getRange();
- if ( name == 'delete' && markerRange.start.isEqual( selection.focus ) ) {
- exceptionDisable.push( name );
- return false;
- }
- if ( name == 'forwardDelete' && selection.isCollapsed && markerRange.end.isEqual( selection.focus ) ) {
- exceptionDisable.push( name );
- return false;
- }
- return true;
- } )
- .map( name => editor.commands.get( name ) );
- for ( const command of commands ) {
- command.clearForceDisabled( 'RestrictedMode' );
- }
- for ( const command of exceptionDisable.map( name => editor.commands.get( name ) ) ) {
- command.forceDisabled( 'RestrictedMode' );
- }
- }
- _disableCommands() {
- const editor = this.editor;
- const commands = this._getCommandNamesToToggle( editor )
- .map( name => editor.commands.get( name ) );
- for ( const command of commands ) {
- command.forceDisabled( 'RestrictedMode' );
- }
- }
- _getCommandNamesToToggle( editor ) {
- return Array.from( editor.commands.names() )
- .filter( name => !this._alwaysEnabled.has( name ) );
- }
- }
- function upcastHighlightToMarker( config ) {
- return dispatcher => dispatcher.on( 'element:span', ( evt, data, conversionApi ) => {
- const { writer } = conversionApi;
- const matcher = new Matcher( config.view );
- const matcherResult = matcher.match( data.viewItem );
- // If there is no match, this callback should not do anything.
- if ( !matcherResult ) {
- return;
- }
- const match = matcherResult.match;
- // Force consuming element's name (taken from upcast helpers elementToElement converter).
- match.name = true;
- const { modelRange: convertedChildrenRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
- conversionApi.consumable.consume( data.viewItem, match );
- const markerName = config.model( data.viewItem );
- const fakeMarkerStart = writer.createElement( '$marker', { 'data-name': markerName } );
- const fakeMarkerEnd = writer.createElement( '$marker', { 'data-name': markerName } );
- // Insert in reverse order to use converter content positions directly (without recalculating).
- writer.insert( fakeMarkerEnd, convertedChildrenRange.end );
- writer.insert( fakeMarkerStart, convertedChildrenRange.start );
- data.modelRange = writer.createRange(
- writer.createPositionBefore( fakeMarkerStart ),
- writer.createPositionAfter( fakeMarkerEnd )
- );
- data.modelCursor = data.modelRange.end;
- } );
- }
- function isSelectionInExceptionMarker( marker, selection ) {
- if ( !marker ) {
- return false;
- }
- const markerRange = marker.getRange();
- if ( selection.isCollapsed ) {
- return isPositionInRangeOrOnRangeBoundary( markerRange, selection.focus );
- }
- return markerRange.containsRange( selection.getFirstRange(), true );
- }
- function isPositionInRangeOrOnRangeBoundary( range, position ) {
- return (
- range.containsPosition( position ) ||
- range.end.isEqual( position ) ||
- range.start.isEqual( position )
- );
- }
|