8
0
Эх сурвалжийг харах

The initial implementation of the widget type around feature.

Aleksander Nowodzinski 5 жил өмнө
parent
commit
690b7badc6

+ 34 - 0
packages/ckeditor5-widget/src/widgettypearound/mouseoveroutobserver.js

@@ -0,0 +1,34 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module widget/widgettypearound/mouseoveroutobserver
+ */
+
+import DomEventObserver from '@ckeditor/ckeditor5-engine/src/view/observer/domeventobserver';
+
+/**
+ * TODO
+ *
+ * @extends module:engine/view/observer/domeventobserver~DomEventObserver
+ */
+export default class MouseOverOutObserver extends DomEventObserver {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( view ) {
+		super( view );
+
+		this.domEventType = [ 'mouseover', 'mouseout' ];
+		this.useCapture = true;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	onDomEvent( domEvent ) {
+		this.fire( domEvent.type, domEvent );
+	}
+}

+ 62 - 0
packages/ckeditor5-widget/src/widgettypearound/utils.js

@@ -0,0 +1,62 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module widget/widgettypearound/utils
+ */
+
+import { isWidget } from '../utils';
+
+export function getClosestTypeAroundDomButton( domElement ) {
+	return domElement.closest( '.ck-widget__type-around__button' );
+}
+
+export function getTypeAroundButtonDirection( domElement ) {
+	return domElement.classList.contains( 'ck-widget__type-around__button_before' ) ? 'before' : 'after';
+}
+
+export function getClosestWidgetViewElement( domElement, domConverter ) {
+	const widgetDomElement = domElement.closest( '.ck-widget' );
+
+	if ( !widgetDomElement ) {
+		return null;
+	}
+
+	return domConverter.mapDomToView( widgetDomElement );
+}
+
+export function directionToWidgetCssClass( direction ) {
+	return `ck-widget_can-type-around_${ direction }`;
+}
+
+export function getWidgetTypeAroundDirections( widgetViewElement ) {
+	const directions = [];
+
+	if ( isFirstChild( widgetViewElement ) || hasPreviousWidgetSibling( widgetViewElement ) ) {
+		directions.push( 'before' );
+	}
+
+	if ( isLastChild( widgetViewElement ) || hasNextWidgetSibling( widgetViewElement ) ) {
+		directions.push( 'after' );
+	}
+
+	return directions;
+}
+
+function isFirstChild( widget ) {
+	return !widget.previousSibling;
+}
+
+function isLastChild( widget ) {
+	return !widget.nextSibling;
+}
+
+function hasPreviousWidgetSibling( widget ) {
+	return widget.previousSibling && isWidget( widget.previousSibling );
+}
+
+function hasNextWidgetSibling( widget ) {
+	return widget.nextSibling && isWidget( widget.nextSibling );
+}

+ 257 - 0
packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js

@@ -0,0 +1,257 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module widget/widgettypearound/widgettypearound
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Template from '@ckeditor/ckeditor5-ui/src/template';
+import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview';
+
+import { isWidget } from '../utils';
+import MouseOverOutObserver from './mouseoveroutobserver';
+import {
+	getWidgetTypeAroundDirections,
+	getClosestTypeAroundDomButton,
+	getTypeAroundButtonDirection,
+	getClosestWidgetViewElement,
+	directionToWidgetCssClass
+} from './utils';
+
+import returnIcon from '../../theme/icons/return-arrow.svg';
+import '../../theme/widgettypearound.css';
+
+const POSSIBLE_INSERTION_DIRECTIONS = [ 'before', 'after' ];
+
+/**
+ * TODO
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class WidgetTypeAround extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ Paragraph ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'WidgetTypeAround';
+	}
+
+	/**
+	 * TODO
+	 */
+	init() {
+		this._enableTypeAroundUIInjection();
+		this._enableDetectionOfTypeAroundWidgets();
+		this._enableUIActivationUsingMouseCursor();
+		// this._enableUIActivationUsingKeyboardArrows(); TODO
+		this._enableInsertingParagraphsOnButtonClick();
+	}
+
+	/**
+	 * TODO
+	 */
+	_enableTypeAroundUIInjection() {
+		const editor = this.editor;
+		const schema = editor.model.schema;
+
+		editor.editing.downcastDispatcher.on( 'insert', ( evt, data, conversionApi ) => {
+			const viewElement = conversionApi.mapper.toViewElement( data.item );
+
+			// Filter out non-widgets and inline widgets.
+			if ( viewElement && isWidget( viewElement ) && !schema.isInline( data.item ) ) {
+				injectUIIntoWidget( editor.editing.view, viewElement );
+			}
+		}, { priority: 'low' } );
+	}
+
+	/**
+	 * TODO
+	 */
+	_enableDetectionOfTypeAroundWidgets() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+
+		editingView.document.registerPostFixer( writer => {
+			// Find all widget view elements in the editing root.
+			[ ...editingView.createRangeIn( editingView.document.getRoot() ) ]
+				.filter( ( { item } ) => isWidget( item ) )
+				.forEach( ( { item: widgetViewElement } ) => {
+					const newDirections = getWidgetTypeAroundDirections( widgetViewElement );
+					const directionClassesToRemove = POSSIBLE_INSERTION_DIRECTIONS
+						.filter( direction => !newDirections.includes( direction ) )
+						.map( directionToWidgetCssClass );
+
+					// Remove classes that do not make sense any more.
+					writer.removeClass( directionClassesToRemove, widgetViewElement );
+
+					// Set CSS classes related to possible directions. They are used so the UI knows
+					// which buttons and lines to display.
+					writer.addClass( newDirections.map( directionToWidgetCssClass ), widgetViewElement );
+
+					// This is needed for the keyboard navigation.
+					writer.setCustomProperty( 'widgetTypeAroundDirections', newDirections, widgetViewElement );
+				} );
+		} );
+	}
+
+	/**
+	 * TODO
+	 */
+	_enableUIActivationUsingMouseCursor() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+
+		editingView.addObserver( MouseOverOutObserver );
+
+		editingView.document.on( 'mouseover', ( evt, domEventData ) => {
+			const button = getClosestTypeAroundDomButton( domEventData.domTarget );
+
+			if ( !button ) {
+				return;
+			}
+
+			const buttonDirection = getTypeAroundButtonDirection( button );
+			const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter );
+
+			if ( widgetViewElement.hasClass( 'ck-widget_type-around_activated' ) ) {
+				return;
+			}
+
+			editingView.change( writer => {
+				writer.addClass( [
+					'ck-widget_type-around_activated',
+					`ck-widget_type-around_activated_${ buttonDirection }`
+				], widgetViewElement );
+			} );
+		} );
+
+		editingView.document.on( 'mouseout', ( evt, { domEvent } ) => {
+			const fromButton = getClosestTypeAroundDomButton( domEvent.target );
+
+			// Happens when blurring the browser window.
+			if ( !domEvent.relatedTarget ) {
+				return;
+			}
+
+			const toButton = getClosestTypeAroundDomButton( domEvent.relatedTarget );
+
+			if ( !fromButton || toButton ) {
+				return;
+			}
+
+			const widgetViewElement = getClosestWidgetViewElement( fromButton, editingView.domConverter );
+
+			editingView.change( writer => {
+				writer.removeClass( [
+					'ck-widget_type-around_activated',
+					'ck-widget_type-around_activated_before',
+					'ck-widget_type-around_activated_after'
+				], widgetViewElement );
+			} );
+		} );
+	}
+
+	/**
+	 * TODO
+	 */
+	_enableInsertingParagraphsOnButtonClick() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+
+		editingView.document.on( 'click', ( evt, domEventData ) => {
+			const button = getClosestTypeAroundDomButton( domEventData.domTarget );
+
+			if ( !button ) {
+				return;
+			}
+
+			const buttonDirection = getTypeAroundButtonDirection( button );
+			const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter );
+			let viewPosition;
+
+			if ( buttonDirection === 'before' ) {
+				viewPosition = editingView.createPositionBefore( widgetViewElement );
+			} else {
+				viewPosition = editingView.createPositionAfter( widgetViewElement );
+			}
+
+			const modelPosition = editor.editing.mapper.toModelPosition( viewPosition );
+
+			editor.model.change( writer => {
+				const paragraph = writer.createElement( 'paragraph' );
+
+				writer.insert( paragraph, modelPosition );
+				writer.setSelection( paragraph, 0 );
+			} );
+
+			editingView.focus();
+			editingView.scrollToTheSelection();
+		} );
+	}
+}
+
+function injectUIIntoWidget( editingView, widgetViewElement ) {
+	editingView.change( writer => {
+		const typeAroundWrapper = writer.createUIElement( 'div', {
+			class: 'ck ck-reset_all ck-widget__type-around'
+		}, function( domDocument ) {
+			const wrapperDomElement = this.toDomElement( domDocument );
+
+			injectButtons( wrapperDomElement );
+			injectLines( wrapperDomElement );
+
+			return wrapperDomElement;
+		} );
+
+		// Inject the type around wrapper into the widget's wrapper.
+		writer.insert( writer.createPositionAt( widgetViewElement, 'end' ), typeAroundWrapper );
+	} );
+}
+
+function injectButtons( wrapperDomElement ) {
+	for ( const direction of POSSIBLE_INSERTION_DIRECTIONS ) {
+		const returnIconView = new IconView();
+		returnIconView.viewBox = '0 0 10 8';
+		returnIconView.content = returnIcon;
+
+		const buttonTemplate = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					`ck-widget__type-around__button ck-widget__type-around__button_${ direction }`
+				]
+			},
+			children: [
+				returnIconView
+			]
+		} );
+
+		wrapperDomElement.appendChild( buttonTemplate.render() );
+	}
+}
+
+function injectLines( wrapperDomElement ) {
+	for ( const direction of POSSIBLE_INSERTION_DIRECTIONS ) {
+		const buttonTemplate = new Template( {
+			tag: 'div',
+			attributes: {
+				class: `ck ck-widget__type-around__line ck-widget__type-around__line_${ direction }`
+			}
+		} );
+
+		wrapperDomElement.appendChild( buttonTemplate.render() );
+	}
+}
+

+ 295 - 0
packages/ckeditor5-widget/theme/widgettypearound.css

@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/*
+ * !!! TODO: Lots of these styles should go to ckeditor5-theme-lark before production.
+ */
+
+:root {
+	--ck-color-widget-type-around-button-active: var(--ck-color-focus-border);
+	--ck-color-widget-type-around-button-hover: var(--ck-color-widget-hover-border);
+	--ck-color-widget-type-around-button-blurred-editable: var(--ck-color-widget-blurred-border);
+	--ck-color-widget-type-around-button-radar-start-alpha: 0;
+	--ck-color-widget-type-around-button-radar-end-alpha: .3;
+	--ck-color-widget-type-around-button-icon: var(--ck-color-base-background);
+	--ck-color-widget-type-around-activated-outline-alpha: .3;
+	--ck-color-widget-type-around-hover-outline-alpha: .6;
+	--ck-color-widget-type-around-line: var(--ck-color-focus-border);
+}
+
+.ck .ck-widget {
+	/*
+	 * Styles of the type around buttons
+	 */
+	& .ck-widget__type-around__button {
+		cursor: pointer;
+		display: none;
+		position: absolute;
+		overflow: hidden;
+		width: 20px;
+		height: 20px;
+
+		z-index: var(--ck-z-default);
+		background: var(--ck-color-widget-type-around-button);
+		border-radius: 100px;
+		animation: fadein linear 300ms 1 normal forwards;
+		transition: opacity 100ms linear;
+
+		& .ck-icon {
+			width: 10px;
+			height: 8px;
+			position: absolute;
+			top: 50%;
+			left: 50%;
+
+			z-index: calc(var(--ck-z-default) + 2);
+			transform: translate(-50%,-50%);
+			transition: transform .5s ease;
+			margin-top: 1px;
+
+			& line,
+			& polyline {
+				stroke-dasharray: 10;
+				stroke-dashoffset: 0;
+
+				fill: none;
+				stroke: var(--ck-color-widget-type-around-button-icon);
+				stroke-width: 1.5px;
+				stroke-linecap: round;
+				stroke-linejoin: round;
+			}
+
+			& line {
+				stroke-dasharray: 7;
+			}
+		}
+
+		&.ck-widget__type-around__button_before {
+			top: -1.5px;
+			left: min(10%, 30px);
+
+			transform: translate(0,-50%);
+		}
+
+		&.ck-widget__type-around__button_after {
+			bottom: -1.5px;
+			right: min(10%, 30px);
+
+			transform: translate(50%,50%);
+		}
+	}
+
+	/*
+	 * Styles of the type around lines.
+	 */
+	& .ck-widget__type-around__line {
+		height: 3px;
+		left: -3px;
+		right: -3px;
+		display: none;
+		position: absolute;
+
+		background: var(--ck-color-widget-type-around-line);
+		animation: ck-widget-type-around-line-blink 1s linear infinite;
+
+		&.ck-widget__type-around__line_before {
+			top: -3px;
+		}
+
+		&.ck-widget__type-around__line_after {
+			bottom: -3px;
+		}
+	}
+
+	/*
+	 * Show type around buttons when the widget gets selected or being hovered.
+	 */
+	&.ck-widget_selected,
+	&:hover {
+		& > .ck-widget__type-around > .ck-widget__type-around__button {
+			display: block;
+		}
+	}
+
+	/*
+	 * Hide the type around buttons depending on which directions the widget supports.
+	 */
+	&:not(.ck-widget_can-type-around_before) > .ck-widget__type-around > .ck-widget__type-around__button_before {
+		display: none;
+	}
+
+	&:not(.ck-widget_can-type-around_after) > .ck-widget__type-around > .ck-widget__type-around__button_after {
+		display: none;
+	}
+
+	/*
+	 * Styles for the buttons when the widget is NOT selected (but, for instance, hovered).
+	 */
+	&:not(.ck-widget_selected) {
+		& > .ck-widget__type-around > .ck-widget__type-around__button {
+			background: var(--ck-color-widget-type-around-button-hover);
+		}
+	}
+
+	/*
+	 * Styles for the buttons when the widget is selected or the button was activated.
+	 */
+	&.ck-widget_selected,
+	&.ck-widget_type-around_activated {
+		& > .ck-widget__type-around > .ck-widget__type-around__button {
+			background: var(--ck-color-widget-type-around-button-active);
+
+			&::after {
+				content: "";
+				width: 18px;
+				height: 18px;
+				display: block;
+				position: absolute;
+				top: 1px;
+				left: 1px;
+
+				border-radius: 100px;
+				z-index: calc(var(--ck-z-default) + 1);
+				background: linear-gradient(135deg, hsla(0,0%,100%,0) 0%, hsla(0,0%,100%,.3) 100%);
+			}
+		}
+	}
+
+	/*
+	 * Styles when the type around becomes activated in a widget.
+	 */
+	&.ck-widget_type-around_activated {
+		/*
+		 * Highlight the type around button when the feature is activated in the widget.
+		 */
+		& > .ck-widget__type-around > .ck-widget__type-around__button {
+			animation: ck-widget-type-around-button-sonar 1s ease infinite;
+
+			/*
+			 * Animate active button's icon.
+			 */
+			& .ck-icon {
+				& polyline {
+					animation: ck-widget-type-around-arrow-dash 2s linear infinite;
+				}
+
+				& line {
+					animation: ck-widget-type-around-arrow-tip-dash 2s linear infinite;
+				}
+			}
+		}
+
+		/*
+		 * Show the "upper" button if the type around is "before" the widget.
+		 * Show the "upper" blinking line if the type around is "before" the widget.
+		 */
+		&.ck-widget_type-around_activated_before > .ck-widget__type-around {
+			& > .ck-widget__type-around__button_before {
+				opacity: 1;
+			}
+
+			& > .ck-widget__type-around__button_after {
+				opacity: 0;
+			}
+
+			& > .ck-widget__type-around__line_before {
+				display: block;
+			}
+		}
+
+		/*
+		 * Show the "lower" button if the type around is "after" the widget.
+		 * Show the "lower" blinking line if the type around is "after" the widget.
+		 */
+		&.ck-widget_type-around_activated_after > .ck-widget__type-around {
+			& > .ck-widget__type-around__button_after {
+				opacity: 1;
+			}
+
+			& > .ck-widget__type-around__button_before {
+				opacity: 0;
+			}
+
+			& > .ck-widget__type-around__line_after {
+				display: block;
+			}
+		}
+	}
+
+	/*
+	 * Widget outline style when the widget is selected.
+	 */
+	&.ck-widget_type-around_activated,
+	&.ck-widget_type-around_activated:hover,
+	&.ck-widget_selected.ck-widget_type-around_activated,
+	&.ck-widget_selected.ck-widget_type-around_activated:hover {
+		outline-color: hsla(var(--ck-color-focus-border-coordinates),var(--ck-color-widget-type-around-activated-outline-alpha));
+	}
+
+	&:not(.ck-widget_selected).ck-widget_type-around_activated,
+	&:not(.ck-widget_selected).ck-widget_type-around_activated:hover {
+		outline-color: hsla(var(--ck-color-widget-hover-border-coordinates),var(--ck-color-widget-type-around-hover-outline-alpha));
+	}
+
+	&.ck-widget_with-selection-handle {
+		&.ck-widget_type-around_activated,
+		&.ck-widget_type-around_activated:hover,
+		&.ck-widget_selected.ck-widget_type-around_activated,
+		&.ck-widget_selected.ck-widget_type-around_activated:hover {
+			& > .ck-widget__selection-handle {
+				background: hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-activated-outline-alpha));
+			}
+		}
+	}
+}
+
+/*
+ * Styles for the buttons when the widget is selected but the user clicked outside of the editor (blurred the editor).
+ */
+.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected > .ck-widget__type-around > .ck-widget__type-around__button:not(:hover) {
+	background: var(--ck-color-widget-type-around-button-blurred-editable);
+}
+
+@keyframes ck-widget-type-around-line-blink {
+	0% {
+		opacity: 0;
+	}
+	30%, 50%, 70% {
+		opacity: 1;
+	}
+	100% {
+		opacity: 0;
+	}
+}
+
+@keyframes ck-widget-type-around-arrow-dash {
+	0% {
+		stroke-dashoffset: 10;
+	}
+	20%, 100% {
+		stroke-dashoffset: 0;
+	}
+}
+
+@keyframes ck-widget-type-around-arrow-tip-dash {
+	0%, 20% {
+		stroke-dashoffset: 7;
+	}
+	40%, 100% {
+		stroke-dashoffset: 0;
+	}
+}
+
+@keyframes ck-widget-type-around-button-sonar {
+	0% {
+		box-shadow: 0 0 0 0 hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));
+	}
+	50% {
+		box-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-end-alpha));
+	}
+	100% {
+		box-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));
+	}
+}