浏览代码

Merge branch 'master' into t/18

Mateusz Samsel 6 年之前
父节点
当前提交
ea2d365aaf

+ 3 - 0
packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js

@@ -6,9 +6,12 @@
 /* globals window */
 
 import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+import BalloonEditor from '@ckeditor/ckeditor5-build-balloon/src/ckeditor';
 
 import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount';
 
 ClassicEditor.builtinPlugins.push( WordCount );
+BalloonEditor.builtinPlugins.push( WordCount );
 
 window.ClassicEditor = ClassicEditor;
+window.BalloonEditor = BalloonEditor;

+ 69 - 26
packages/ckeditor5-word-count/docs/_snippets/features/word-count-update.html

@@ -1,37 +1,80 @@
-
 <style>
-	.customized-count__color-box {
-		--hue: 56;
-		width: 20px;
-		height: 20px;
-		background-color: hsl( var( --hue ), 100%, 50% );
-		display: inline-block;
-		vertical-align: middle;
-		border-radius: 100%;
+	.demo-update {
+		border: 1px solid var(--ck-color-base-border);
+		border-radius: var(--ck-border-radius);
+		box-shadow: 2px 2px 0px hsla( 0, 0%, 0%, 0.1 );
+		margin: 1.5em 0;
+		padding: 1em;
+	}
+
+	.demo-update h3 {
+		font-size: 18px;
+		font-weight: bold;
+		margin: 0 0 .5em;
+		padding: 0;
+	}
+
+	.demo-update .ck.ck-editor__editable_inline {
+		border: 1px solid hsla( 0, 0%, 0%, 0.15 );
+		transition: background .5s ease-out;
+		min-height: 6em;
+		margin-bottom: 1em;
+	}
+
+	.demo-update__controls {
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+	}
+
+	.demo-update__chart {
+		margin-right: 1em;
+	}
+
+	.demo-update__chart__circle {
+		transform: rotate(-90deg);
+		transform-origin: center;
+	}
+
+	.demo-update__chart__characters {
+		font-size: 13px;
+		font-weight: bold;
+	}
+
+	.demo-update__words {
+		flex-grow: 1;
+		opacity: .5;
 	}
 
-	.customized-count > div {
-		margin-left: 1em;
+	.demo-update__limit-close .demo-update__chart__circle {
+		stroke: hsl( 30, 100%, 52% );
 	}
 
-	.customized-count span {
-		min-width: 90px;
-		display: inline-block;
+	.demo-update__limit-exceeded .ck.ck-editor__editable_inline {
+		background: hsl( 0, 100%, 97% );
+	}
+
+	.demo-update__limit-exceeded .demo-update__chart__circle {
+		stroke: hsl( 0, 100%, 52% );
+	}
+
+	.demo-update__limit-exceeded .demo-update__chart__characters {
+		fill: hsl( 0, 100%, 52% );
 	}
 </style>
 
-<div id="demo-editor-update">
-	<p>Tourists frequently admit that <a href="https://en.wikipedia.org/wiki/Taj_Mahal">Taj Mahal</a> “simply cannot be described with words”.</p>
-</div>
-<div class="word-count customized-count">
-	<div class="customized-count__words">
-		<label>
-			<span>Words:</span>
-			<progress max="20"></progress>
-		</label>
+<div class="demo-update">
+	<h3>Post editor with word count</h3>
+	<div id="demo-update__editor">
+		<p>Tourists frequently admit that <a href="https://en.wikipedia.org/wiki/Taj_Mahal">Taj Mahal</a> “simply cannot be described with words”.</p>
 	</div>
-	<div class="customized-count__characters">
-		<span>Characters:</span>
-		<div class="customized-count__color-box"></div>
+	<div class="demo-update__controls">
+		<span class="demo-update__words"></span>
+		<svg class="demo-update__chart" viewbox="0 0 40 40" width="40" height="40" xmlns="http://www.w3.org/2000/svg">
+			<circle stroke="hsl(0, 0%, 93%)" stroke-width="3" fill="none" cx="20" cy="20" r="17" />
+			<circle class="demo-update__chart__circle" stroke="hsl(202, 92%, 59%)" stroke-width="3" stroke-dasharray="134,534" stroke-linecap="round" fill="none" cx="20" cy="20" r="17" />
+			<text class="demo-update__chart__characters" x="50%" y="50%" dominant-baseline="central" text-anchor="middle"></text>
+		</svg>
+		<button type="button" class="demo-update__send">Send post</button>
 	</div>
 </div>

+ 43 - 27
packages/ckeditor5-word-count/docs/_snippets/features/word-count-update.js

@@ -3,10 +3,11 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* global window, document, console, ClassicEditor */
+/* global window, document, console, BalloonEditor */
+const sendButton = document.querySelector( '.demo-update__send' );
 
-ClassicEditor
-	.create( document.querySelector( '#demo-editor-update' ), {
+BalloonEditor
+	.create( document.querySelector( '#demo-update__editor' ), {
 		toolbar: {
 			items: [
 				'heading',
@@ -26,43 +27,58 @@ ClassicEditor
 			],
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},
+		placeholder: 'Text of the post',
 		table: {
 			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
 		},
 		wordCount: {
 			onUpdate: ( () => {
-				const progressBar = document.querySelector( '.customized-count progress' );
-				const colorBox = document.querySelector( '.customized-count__color-box' );
+				const maxCharacters = 120;
+				const container = document.querySelector( '.demo-update' );
+				const progressCircle = document.querySelector( '.demo-update__chart__circle' );
+				const charactersBox = document.querySelector( '.demo-update__chart__characters' );
+				const wordsBox = document.querySelector( '.demo-update__words' );
+				const circleCircumference = Math.floor( 2 * Math.PI * progressCircle.getAttribute( 'r' ) );
 
+				// Update the UI as the content of the editor changes.
 				return data => {
-					const charactersHue = calculateHue( {
-						characters: data.characters,
-						greenUntil: 70,
-						maxCharacters: 120
-					} );
+					const currentCharacters = data.characters;
+					const currentWords = data.words;
+					const charactersProgress = currentCharacters / maxCharacters * circleCircumference;
+					const isLimitExceeded = currentCharacters > maxCharacters;
+					const isCloseToLimit = !isLimitExceeded && currentCharacters > maxCharacters * .8;
+					const circleDashArray = Math.min( charactersProgress, circleCircumference );
 
-					progressBar.value = data.words;
-					colorBox.style.setProperty( '--hue', charactersHue );
-				};
+					// Set the stroke of the circle to show the how many characters were typed.
+					progressCircle.setAttribute( 'stroke-dasharray', `${ circleDashArray },${ circleCircumference }` );
+
+					// Display the number of characters in the progress chart. When exceeded the limit,
+					// display how many characters should be removed.
+					if ( isLimitExceeded ) {
+						charactersBox.textContent = `-${ currentCharacters - maxCharacters }`;
+					} else {
+						charactersBox.textContent = currentCharacters;
+					}
+
+					wordsBox.textContent = `Words in the post: ${ currentWords }`;
 
-				// Calculates the hue based on the number of characters.
-				//
-				// For the character counter:
-				//
-				// * below greenUntil - Returns green.
-				// * between greenUntil and maxCharacters - Returns a hue between green and red.
-				// * above maxCharacters - Returns red.
-				function calculateHue( { characters, greenUntil, maxCharacters } ) {
-					const greenHue = 70;
-					const redHue = 0;
-					const progress = Math.max( 0, Math.min( 1, ( characters - greenUntil ) / ( maxCharacters - greenUntil ) ) ); // 0-1
-					const discreetProgress = Math.floor( progress * 10 ) / 10; // 0, 0.1, 0.2, ..., 1
+					// If the content length is close to the characters limit, add a CSS class to warns the user.
+					container.classList.toggle( 'demo-update__limit-close', isCloseToLimit );
 
-					return ( redHue - greenHue ) * discreetProgress + greenHue;
-				}
+					// If exceeded the characters limit, add a CSS class that makes the content's background red.
+					container.classList.toggle( 'demo-update__limit-exceeded', isLimitExceeded );
+
+					// If exceeded the characters limit, disable the send button.
+					sendButton.toggleAttribute( 'disabled', isLimitExceeded );
+				};
 			} )()
 		}
 	} )
+	.then( () => {
+		sendButton.addEventListener( 'click', () => {
+			window.alert( 'Post sent!' ); // eslint-disable-line no-alert
+		} );
+	} )
 	.catch( err => {
 		console.error( err.stack );
 	} );

文件差异内容过多而无法显示
+ 153 - 46
packages/ckeditor5-word-count/docs/features/word-count.md


+ 45 - 7
packages/ckeditor5-word-count/src/wordcount.js

@@ -69,6 +69,22 @@ export default class WordCount extends Plugin {
 		 */
 		this.set( 'words', 0 );
 
+		// Don't wait for #update event to set the value of the properties but obtain it right away.
+		// This way, accessing the properties directly returns precise numbers, e.g. for validation, etc..
+		// If not accessed directly, the properties will be refreshed upon #update anyway.
+		Object.defineProperties( this, {
+			characters: {
+				get() {
+					return ( this.characters = this._getCharacters() );
+				}
+			},
+			words: {
+				get() {
+					return ( this.words = this._getWords() );
+				}
+			}
+		} );
+
 		/**
 		 * The label used to display the words value in the {@link #wordCountContainer output container}.
 		 *
@@ -239,6 +255,31 @@ export default class WordCount extends Plugin {
 	}
 
 	/**
+	 * Determines the number of characters in the current editor's model.
+	 *
+	 * @private
+	 * @returns {Number}
+	 */
+	_getCharacters() {
+		const txt = modelElementToPlainText( this.editor.model.document.getRoot() );
+
+		return txt.replace( /\n/g, '' ).length;
+	}
+
+	/**
+	 * Determines the number of words in the current editor's model.
+	 *
+	 * @private
+	 * @returns {Number}
+	 */
+	_getWords() {
+		const txt = modelElementToPlainText( this.editor.model.document.getRoot() );
+		const detectedWords = txt.match( this._wordsMatchRegExp ) || [];
+
+		return detectedWords.length;
+	}
+
+	/**
 	 * Determines the number of words and characters in the current editor's model and assigns it to {@link #characters} and {@link #words}.
 	 * It also fires the {@link #event:update}.
 	 *
@@ -246,15 +287,12 @@ export default class WordCount extends Plugin {
 	 * @fires update
 	 */
 	_refreshStats() {
-		const txt = modelElementToPlainText( this.editor.model.document.getRoot() );
-		const detectedWords = txt.match( this._wordsMatchRegExp ) || [];
-
-		this.characters = txt.replace( /\n/g, '' ).length;
-		this.words = detectedWords.length;
+		const words = this.words = this._getWords();
+		const characters = this.characters = this._getCharacters();
 
 		this.fire( 'update', {
-			words: this.words,
-			characters: this.characters
+			words,
+			characters
 		} );
 	}
 }

+ 8 - 0
packages/ckeditor5-word-count/tests/manual/wordcount.js

@@ -28,6 +28,14 @@ ClassicEditor
 		document.getElementById( 'destroy-editor' ).addEventListener( 'click', () => {
 			editor.destroy();
 		} );
+
+		editor.plugins.get( 'WordCount' ).on( 'change:words', ( evt, name, value ) => {
+			console.log( 'WordCount:change:words', value );
+		} );
+
+		editor.plugins.get( 'WordCount' ).on( 'change:characters', ( evt, name, value ) => {
+			console.log( 'WordCount:change:characters', value );
+		} );
 	} )
 	.catch( err => {
 		console.error( err.stack );

+ 73 - 7
packages/ckeditor5-word-count/tests/wordcount.js

@@ -39,15 +39,81 @@ describe( 'WordCount', () => {
 	} );
 
 	describe( 'constructor()', () => {
-		it( 'has defined "words" property', () => {
-			expect( wordCountPlugin.words ).to.equal( 0 );
+		describe( '#words property', () => {
+			it( 'is defined', () => {
+				expect( wordCountPlugin.words ).to.equal( 0 );
+			} );
+
+			it( 'returns the number of words right away', () => {
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
+				expect( wordCountPlugin.words ).to.equal( 2 );
+
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text> world</paragraph>' );
+				expect( wordCountPlugin.words ).to.equal( 2 );
+
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text></paragraph>' );
+				expect( wordCountPlugin.words ).to.equal( 1 );
+
+				setModelData( model, '' );
+				expect( wordCountPlugin.words ).to.equal( 0 );
+			} );
+
+			it( 'is observable', done => {
+				const spy = sinon.spy();
+
+				wordCountPlugin.on( 'change:words', spy );
+
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text></paragraph>' );
+
+				setTimeout( () => {
+					// The #update event fired once because it is throttled.
+					sinon.assert.calledOnce( spy );
+					expect( spy.firstCall.args[ 2 ] ).to.equal( 1 );
+
+					done();
+				}, DELAY );
+			} );
 		} );
 
-		it( 'has defined "characters" property', () => {
-			expect( wordCountPlugin.characters ).to.equal( 0 );
+		describe( '#characters property', () => {
+			it( 'is defined', () => {
+				expect( wordCountPlugin.characters ).to.equal( 0 );
+			} );
+
+			it( 'returns the number of characters right away', () => {
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
+				expect( wordCountPlugin.characters ).to.equal( 12 );
+
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text> world</paragraph>' );
+				expect( wordCountPlugin.characters ).to.equal( 11 );
+
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text></paragraph>' );
+				expect( wordCountPlugin.characters ).to.equal( 5 );
+
+				setModelData( model, '' );
+				expect( wordCountPlugin.characters ).to.equal( 0 );
+			} );
+
+			it( 'is observable', done => {
+				const spy = sinon.spy();
+
+				wordCountPlugin.on( 'change:characters', spy );
+
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
+				setModelData( model, '<paragraph><$text foo="true">Hello</$text></paragraph>' );
+
+				setTimeout( () => {
+					// The #update event fired once because it is throttled.
+					sinon.assert.calledOnce( spy );
+					expect( spy.firstCall.args[ 2 ] ).to.equal( 5 );
+
+					done();
+				}, DELAY );
+			} );
 		} );
 
-		it( 'has "WordCount" plugin name', () => {
+		it( 'has a name', () => {
 			expect( WordCount.pluginName ).to.equal( 'WordCount' );
 		} );
 	} );
@@ -130,8 +196,8 @@ describe( 'WordCount', () => {
 			} );
 		} );
 
-		describe( 'update event', () => {
-			it( 'fires update event with actual amount of characters and words', () => {
+		describe( '#update event', () => {
+			it( 'fires with the actual number of characters and words', () => {
 				const fake = sinon.fake();
 				wordCountPlugin.on( 'update', fake );