@ckeditor/ckeditor5-block-quote 35.4.0 → 36.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/LICENSE.md CHANGED
@@ -2,7 +2,7 @@ Software License Agreement
2
2
  ==========================
3
3
 
4
4
  **CKEditor 5 block quote feature** – https://github.com/ckeditor/ckeditor5-block-quote <br>
5
- Copyright (c) 2003-2022, [CKSource Holding sp. z o.o.](https://cksource.com) All rights reserved.
5
+ Copyright (c) 2003-2023, [CKSource Holding sp. z o.o.](https://cksource.com) All rights reserved.
6
6
 
7
7
  Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
8
8
 
@@ -1,5 +1,5 @@
1
1
  !function(e){const t=e.en=e.en||{};t.dictionary=Object.assign(t.dictionary||{},{"Block quote":"Block quote"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={})),
2
2
  /*!
3
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
4
4
  * For licensing, see LICENSE.md.
5
5
  */(()=>{var e={446:(e,t,o)=>{"use strict";o.d(t,{Z:()=>i});var n=o(609),r=o.n(n)()((function(e){return e[1]}));r.push([e.id,".ck-content blockquote{border-left:5px solid #ccc;font-style:italic;margin-left:0;margin-right:0;overflow:hidden;padding-left:1.5em;padding-right:1.5em}.ck-content[dir=rtl] blockquote{border-left:0;border-right:5px solid #ccc}",""]);const i=r},609:e=>{"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var o=e(t);return t[2]?"@media ".concat(t[2]," {").concat(o,"}"):o})).join("")},t.i=function(e,o,n){"string"==typeof e&&(e=[[null,e,""]]);var r={};if(n)for(var i=0;i<this.length;i++){var c=this[i][0];null!=c&&(r[c]=!0)}for(var s=0;s<e.length;s++){var l=[].concat(e[s]);n&&r[l[0]]||(o&&(l[2]?l[2]="".concat(o," and ").concat(l[2]):l[2]=o),t.push(l))}},t}},62:(e,t,o)=>{"use strict";var n,r=function(){return void 0===n&&(n=Boolean(window&&document&&document.all&&!window.atob)),n},i=function(){var e={};return function(t){if(void 0===e[t]){var o=document.querySelector(t);if(window.HTMLIFrameElement&&o instanceof window.HTMLIFrameElement)try{o=o.contentDocument.head}catch(e){o=null}e[t]=o}return e[t]}}(),c=[];function s(e){for(var t=-1,o=0;o<c.length;o++)if(c[o].identifier===e){t=o;break}return t}function l(e,t){for(var o={},n=[],r=0;r<e.length;r++){var i=e[r],l=t.base?i[0]+t.base:i[0],a=o[l]||0,u="".concat(l," ").concat(a);o[l]=a+1;var d=s(u),f={css:i[1],media:i[2],sourceMap:i[3]};-1!==d?(c[d].references++,c[d].updater(f)):c.push({identifier:u,updater:v(f,t),references:1}),n.push(u)}return n}function a(e){var t=document.createElement("style"),n=e.attributes||{};if(void 0===n.nonce){var r=o.nc;r&&(n.nonce=r)}if(Object.keys(n).forEach((function(e){t.setAttribute(e,n[e])})),"function"==typeof e.insert)e.insert(t);else{var c=i(e.insert||"head");if(!c)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");c.appendChild(t)}return t}var u,d=(u=[],function(e,t){return u[e]=t,u.filter(Boolean).join("\n")});function f(e,t,o,n){var r=o?"":n.media?"@media ".concat(n.media," {").concat(n.css,"}"):n.css;if(e.styleSheet)e.styleSheet.cssText=d(t,r);else{var i=document.createTextNode(r),c=e.childNodes;c[t]&&e.removeChild(c[t]),c.length?e.insertBefore(i,c[t]):e.appendChild(i)}}function p(e,t,o){var n=o.css,r=o.media,i=o.sourceMap;if(r?e.setAttribute("media",r):e.removeAttribute("media"),i&&"undefined"!=typeof btoa&&(n+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(i))))," */")),e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}var m=null,h=0;function v(e,t){var o,n,r;if(t.singleton){var i=h++;o=m||(m=a(t)),n=f.bind(null,o,i,!1),r=f.bind(null,o,i,!0)}else o=a(t),n=p.bind(null,o,t),r=function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(o)};return n(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;n(e=t)}else r()}}e.exports=function(e,t){(t=t||{}).singleton||"boolean"==typeof t.singleton||(t.singleton=r());var o=l(e=e||[],t);return function(e){if(e=e||[],"[object Array]"===Object.prototype.toString.call(e)){for(var n=0;n<o.length;n++){var r=s(o[n]);c[r].references--}for(var i=l(e,t),a=0;a<o.length;a++){var u=s(o[a]);0===c[u].references&&(c[u].updater(),c.splice(u,1))}o=i}}}},704:(e,t,o)=>{e.exports=o(79)("./src/core.js")},331:(e,t,o)=>{e.exports=o(79)("./src/enter.js")},181:(e,t,o)=>{e.exports=o(79)("./src/typing.js")},273:(e,t,o)=>{e.exports=o(79)("./src/ui.js")},209:(e,t,o)=>{e.exports=o(79)("./src/utils.js")},79:e=>{"use strict";e.exports=CKEditor5.dll}},t={};function o(n){var r=t[n];if(void 0!==r)return r.exports;var i=t[n]={id:n,exports:{}};return e[n](i,i.exports,o),i.exports}o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nc=void 0;var n={};(()=>{"use strict";o.r(n),o.d(n,{BlockQuote:()=>g,BlockQuoteEditing:()=>u,BlockQuoteUI:()=>v});var e=o(704),t=o(331),r=o(181),i=o(209);class c extends e.Command{refresh(){this.value=this._getValue(),this.isEnabled=this._checkEnabled()}execute(e={}){const t=this.editor.model,o=t.schema,n=t.document.selection,r=Array.from(n.getSelectedBlocks()),i=void 0===e.forceValue?!this.value:e.forceValue;t.change((e=>{if(i){const t=r.filter((e=>s(e)||a(o,e)));this._applyQuote(e,t)}else this._removeQuote(e,r.filter(s))}))}_getValue(){const e=this.editor.model.document.selection,t=(0,i.first)(e.getSelectedBlocks());return!(!t||!s(t))}_checkEnabled(){if(this.value)return!0;const e=this.editor.model.document.selection,t=this.editor.model.schema,o=(0,i.first)(e.getSelectedBlocks());return!!o&&a(t,o)}_removeQuote(e,t){l(e,t).reverse().forEach((t=>{if(t.start.isAtStart&&t.end.isAtEnd)return void e.unwrap(t.start.parent);if(t.start.isAtStart){const o=e.createPositionBefore(t.start.parent);return void e.move(t,o)}t.end.isAtEnd||e.split(t.end);const o=e.createPositionAfter(t.end.parent);e.move(t,o)}))}_applyQuote(e,t){const o=[];l(e,t).reverse().forEach((t=>{let n=s(t.start);n||(n=e.createElement("blockQuote"),e.wrap(t,n)),o.push(n)})),o.reverse().reduce(((t,o)=>t.nextSibling==o?(e.merge(e.createPositionAfter(t)),t):o))}}function s(e){return"blockQuote"==e.parent.name?e.parent:null}function l(e,t){let o,n=0;const r=[];for(;n<t.length;){const i=t[n],c=t[n+1];o||(o=e.createPositionBefore(i)),c&&i.nextSibling==c||(r.push(e.createRange(o,e.createPositionAfter(i))),o=null),n++}return r}function a(e,t){const o=e.checkChild(t.parent,"blockQuote"),n=e.checkChild(["$root","blockQuote"],t);return o&&n}class u extends e.Plugin{static get pluginName(){return"BlockQuoteEditing"}static get requires(){return[t.Enter,r.Delete]}init(){const e=this.editor,t=e.model.schema;e.commands.add("blockQuote",new c(e)),t.register("blockQuote",{inheritAllFrom:"$container"}),e.conversion.elementToElement({model:"blockQuote",view:"blockquote"}),e.model.document.registerPostFixer((o=>{const n=e.model.document.differ.getChanges();for(const e of n)if("insert"==e.type){const n=e.position.nodeAfter;if(!n)continue;if(n.is("element","blockQuote")&&n.isEmpty)return o.remove(n),!0;if(n.is("element","blockQuote")&&!t.checkChild(e.position,n))return o.unwrap(n),!0;if(n.is("element")){const e=o.createRangeIn(n);for(const n of e.getItems())if(n.is("element","blockQuote")&&!t.checkChild(o.createPositionBefore(n),n))return o.unwrap(n),!0}}else if("remove"==e.type){const t=e.position.parent;if(t.is("element","blockQuote")&&t.isEmpty)return o.remove(t),!0}return!1}));const o=this.editor.editing.view.document,n=e.model.document.selection,r=e.commands.get("blockQuote");this.listenTo(o,"enter",((t,o)=>{if(!n.isCollapsed||!r.value)return;n.getLastPosition().parent.isEmpty&&(e.execute("blockQuote"),e.editing.view.scrollToTheSelection(),o.preventDefault(),t.stop())}),{context:"blockquote"}),this.listenTo(o,"delete",((t,o)=>{if("backward"!=o.direction||!n.isCollapsed||!r.value)return;const i=n.getLastPosition().parent;i.isEmpty&&!i.previousSibling&&(e.execute("blockQuote"),e.editing.view.scrollToTheSelection(),o.preventDefault(),t.stop())}),{context:"blockquote"})}}var d=o(273),f=o(62),p=o.n(f),m=o(446),h={injectType:"singletonStyleTag",attributes:{"data-cke":!0},insert:"head",singleton:!0};p()(m.Z,h);m.Z.locals;class v extends e.Plugin{static get pluginName(){return"BlockQuoteUI"}init(){const t=this.editor,o=t.t;t.ui.componentFactory.add("blockQuote",(n=>{const r=t.commands.get("blockQuote"),i=new d.ButtonView(n);return i.set({label:o("Block quote"),icon:e.icons.quote,tooltip:!0,isToggleable:!0}),i.bind("isOn","isEnabled").to(r,"value","isEnabled"),this.listenTo(i,"execute",(()=>{t.execute("blockQuote"),t.editing.view.focus()})),i}))}}class g extends e.Plugin{static get requires(){return[u,v]}static get pluginName(){return"BlockQuote"}}})(),(window.CKEditor5=window.CKEditor5||{}).blockQuote=n})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-block-quote",
3
- "version": "35.4.0",
3
+ "version": "36.0.0",
4
4
  "description": "Block quote feature for CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -12,22 +12,23 @@
12
12
  ],
13
13
  "main": "src/index.js",
14
14
  "dependencies": {
15
- "ckeditor5": "^35.4.0"
15
+ "ckeditor5": "^36.0.0"
16
16
  },
17
17
  "devDependencies": {
18
- "@ckeditor/ckeditor5-basic-styles": "^35.4.0",
19
- "@ckeditor/ckeditor5-core": "^35.4.0",
20
- "@ckeditor/ckeditor5-dev-utils": "^31.0.0",
21
- "@ckeditor/ckeditor5-editor-classic": "^35.4.0",
22
- "@ckeditor/ckeditor5-engine": "^35.4.0",
23
- "@ckeditor/ckeditor5-enter": "^35.4.0",
24
- "@ckeditor/ckeditor5-heading": "^35.4.0",
25
- "@ckeditor/ckeditor5-image": "^35.4.0",
26
- "@ckeditor/ckeditor5-list": "^35.4.0",
27
- "@ckeditor/ckeditor5-paragraph": "^35.4.0",
28
- "@ckeditor/ckeditor5-table": "^35.4.0",
29
- "@ckeditor/ckeditor5-theme-lark": "^35.4.0",
30
- "@ckeditor/ckeditor5-typing": "^35.4.0",
18
+ "@ckeditor/ckeditor5-basic-styles": "^36.0.0",
19
+ "@ckeditor/ckeditor5-core": "^36.0.0",
20
+ "@ckeditor/ckeditor5-dev-utils": "^32.0.0",
21
+ "@ckeditor/ckeditor5-editor-classic": "^36.0.0",
22
+ "@ckeditor/ckeditor5-engine": "^36.0.0",
23
+ "@ckeditor/ckeditor5-enter": "^36.0.0",
24
+ "@ckeditor/ckeditor5-heading": "^36.0.0",
25
+ "@ckeditor/ckeditor5-image": "^36.0.0",
26
+ "@ckeditor/ckeditor5-list": "^36.0.0",
27
+ "@ckeditor/ckeditor5-paragraph": "^36.0.0",
28
+ "@ckeditor/ckeditor5-table": "^36.0.0",
29
+ "@ckeditor/ckeditor5-theme-lark": "^36.0.0",
30
+ "@ckeditor/ckeditor5-typing": "^36.0.0",
31
+ "typescript": "^4.8.4",
31
32
  "webpack": "^5.58.1",
32
33
  "webpack-cli": "^4.9.0"
33
34
  },
@@ -46,13 +47,16 @@
46
47
  },
47
48
  "files": [
48
49
  "lang",
49
- "src",
50
+ "src/**/*.js",
51
+ "src/**/*.d.ts",
50
52
  "theme",
51
53
  "build",
52
54
  "ckeditor5-metadata.json",
53
55
  "CHANGELOG.md"
54
56
  ],
55
57
  "scripts": {
56
- "dll:build": "webpack"
58
+ "dll:build": "webpack",
59
+ "build": "tsc -p ./tsconfig.release.json",
60
+ "postversion": "npm run build"
57
61
  }
58
62
  }
package/src/blockquote.js CHANGED
@@ -1,17 +1,13 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module block-quote/blockquote
8
7
  */
9
-
10
8
  import { Plugin } from 'ckeditor5/src/core';
11
-
12
9
  import BlockQuoteEditing from './blockquoteediting';
13
10
  import BlockQuoteUI from './blockquoteui';
14
-
15
11
  /**
16
12
  * The block quote plugin.
17
13
  *
@@ -23,17 +19,16 @@ import BlockQuoteUI from './blockquoteui';
23
19
  * @extends module:core/plugin~Plugin
24
20
  */
25
21
  export default class BlockQuote extends Plugin {
26
- /**
27
- * @inheritDoc
28
- */
29
- static get requires() {
30
- return [ BlockQuoteEditing, BlockQuoteUI ];
31
- }
32
-
33
- /**
34
- * @inheritDoc
35
- */
36
- static get pluginName() {
37
- return 'BlockQuote';
38
- }
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ static get requires() {
26
+ return [BlockQuoteEditing, BlockQuoteUI];
27
+ }
28
+ /**
29
+ * @inheritDoc
30
+ */
31
+ static get pluginName() {
32
+ return 'BlockQuote';
33
+ }
39
34
  }
@@ -1,232 +1,172 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module block-quote/blockquotecommand
8
7
  */
9
-
10
8
  import { Command } from 'ckeditor5/src/core';
11
9
  import { first } from 'ckeditor5/src/utils';
12
-
13
10
  /**
14
11
  * The block quote command plugin.
15
12
  *
16
13
  * @extends module:core/command~Command
17
14
  */
18
15
  export default class BlockQuoteCommand extends Command {
19
- /**
20
- * Whether the selection starts in a block quote.
21
- *
22
- * @observable
23
- * @readonly
24
- * @member {Boolean} #value
25
- */
26
-
27
- /**
28
- * @inheritDoc
29
- */
30
- refresh() {
31
- this.value = this._getValue();
32
- this.isEnabled = this._checkEnabled();
33
- }
34
-
35
- /**
36
- * Executes the command. When the command {@link #value is on}, all top-most block quotes within
37
- * the selection will be removed. If it is off, all selected blocks will be wrapped with
38
- * a block quote.
39
- *
40
- * @fires execute
41
- * @param {Object} [options] Command options.
42
- * @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will apply a block quote,
43
- * otherwise the command will remove the block quote. If not set, the command will act basing on its current value.
44
- */
45
- execute( options = {} ) {
46
- const model = this.editor.model;
47
- const schema = model.schema;
48
- const selection = model.document.selection;
49
-
50
- const blocks = Array.from( selection.getSelectedBlocks() );
51
-
52
- const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;
53
-
54
- model.change( writer => {
55
- if ( !value ) {
56
- this._removeQuote( writer, blocks.filter( findQuote ) );
57
- } else {
58
- const blocksToQuote = blocks.filter( block => {
59
- // Already quoted blocks needs to be considered while quoting too
60
- // in order to reuse their <bQ> elements.
61
- return findQuote( block ) || checkCanBeQuoted( schema, block );
62
- } );
63
-
64
- this._applyQuote( writer, blocksToQuote );
65
- }
66
- } );
67
- }
68
-
69
- /**
70
- * Checks the command's {@link #value}.
71
- *
72
- * @private
73
- * @returns {Boolean} The current value.
74
- */
75
- _getValue() {
76
- const selection = this.editor.model.document.selection;
77
-
78
- const firstBlock = first( selection.getSelectedBlocks() );
79
-
80
- // In the current implementation, the block quote must be an immediate parent of a block element.
81
- return !!( firstBlock && findQuote( firstBlock ) );
82
- }
83
-
84
- /**
85
- * Checks whether the command can be enabled in the current context.
86
- *
87
- * @private
88
- * @returns {Boolean} Whether the command should be enabled.
89
- */
90
- _checkEnabled() {
91
- if ( this.value ) {
92
- return true;
93
- }
94
-
95
- const selection = this.editor.model.document.selection;
96
- const schema = this.editor.model.schema;
97
-
98
- const firstBlock = first( selection.getSelectedBlocks() );
99
-
100
- if ( !firstBlock ) {
101
- return false;
102
- }
103
-
104
- return checkCanBeQuoted( schema, firstBlock );
105
- }
106
-
107
- /**
108
- * Removes the quote from given blocks.
109
- *
110
- * If blocks which are supposed to be "unquoted" are in the middle of a quote,
111
- * start it or end it, then the quote will be split (if needed) and the blocks
112
- * will be moved out of it, so other quoted blocks remained quoted.
113
- *
114
- * @private
115
- * @param {module:engine/model/writer~Writer} writer
116
- * @param {Array.<module:engine/model/element~Element>} blocks
117
- */
118
- _removeQuote( writer, blocks ) {
119
- // Unquote all groups of block. Iterate in the reverse order to not break following ranges.
120
- getRangesOfBlockGroups( writer, blocks ).reverse().forEach( groupRange => {
121
- if ( groupRange.start.isAtStart && groupRange.end.isAtEnd ) {
122
- writer.unwrap( groupRange.start.parent );
123
-
124
- return;
125
- }
126
-
127
- // The group of blocks are at the beginning of an <bQ> so let's move them left (out of the <bQ>).
128
- if ( groupRange.start.isAtStart ) {
129
- const positionBefore = writer.createPositionBefore( groupRange.start.parent );
130
-
131
- writer.move( groupRange, positionBefore );
132
-
133
- return;
134
- }
135
-
136
- // The blocks are in the middle of an <bQ> so we need to split the <bQ> after the last block
137
- // so we move the items there.
138
- if ( !groupRange.end.isAtEnd ) {
139
- writer.split( groupRange.end );
140
- }
141
-
142
- // Now we are sure that groupRange.end.isAtEnd is true, so let's move the blocks right.
143
-
144
- const positionAfter = writer.createPositionAfter( groupRange.end.parent );
145
-
146
- writer.move( groupRange, positionAfter );
147
- } );
148
- }
149
-
150
- /**
151
- * Applies the quote to given blocks.
152
- *
153
- * @private
154
- * @param {module:engine/model/writer~Writer} writer
155
- * @param {Array.<module:engine/model/element~Element>} blocks
156
- */
157
- _applyQuote( writer, blocks ) {
158
- const quotesToMerge = [];
159
-
160
- // Quote all groups of block. Iterate in the reverse order to not break following ranges.
161
- getRangesOfBlockGroups( writer, blocks ).reverse().forEach( groupRange => {
162
- let quote = findQuote( groupRange.start );
163
-
164
- if ( !quote ) {
165
- quote = writer.createElement( 'blockQuote' );
166
-
167
- writer.wrap( groupRange, quote );
168
- }
169
-
170
- quotesToMerge.push( quote );
171
- } );
172
-
173
- // Merge subsequent <bQ> elements. Reverse the order again because this time we want to go through
174
- // the <bQ> elements in the source order (due to how merge works – it moves the right element's content
175
- // to the first element and removes the right one. Since we may need to merge a couple of subsequent `<bQ>` elements
176
- // we want to keep the reference to the first (furthest left) one.
177
- quotesToMerge.reverse().reduce( ( currentQuote, nextQuote ) => {
178
- if ( currentQuote.nextSibling == nextQuote ) {
179
- writer.merge( writer.createPositionAfter( currentQuote ) );
180
-
181
- return currentQuote;
182
- }
183
-
184
- return nextQuote;
185
- } );
186
- }
16
+ /**
17
+ * @inheritDoc
18
+ */
19
+ refresh() {
20
+ this.value = this._getValue();
21
+ this.isEnabled = this._checkEnabled();
22
+ }
23
+ /**
24
+ * Executes the command. When the command {@link #value is on}, all top-most block quotes within
25
+ * the selection will be removed. If it is off, all selected blocks will be wrapped with
26
+ * a block quote.
27
+ *
28
+ * @fires execute
29
+ * @param options Command options.
30
+ * @param options.forceValue If set, it will force the command behavior. If `true`, the command will apply a block quote,
31
+ * otherwise the command will remove the block quote. If not set, the command will act basing on its current value.
32
+ */
33
+ execute(options = {}) {
34
+ const model = this.editor.model;
35
+ const schema = model.schema;
36
+ const selection = model.document.selection;
37
+ const blocks = Array.from(selection.getSelectedBlocks());
38
+ const value = (options.forceValue === undefined) ? !this.value : options.forceValue;
39
+ model.change(writer => {
40
+ if (!value) {
41
+ this._removeQuote(writer, blocks.filter(findQuote));
42
+ }
43
+ else {
44
+ const blocksToQuote = blocks.filter(block => {
45
+ // Already quoted blocks needs to be considered while quoting too
46
+ // in order to reuse their <bQ> elements.
47
+ return findQuote(block) || checkCanBeQuoted(schema, block);
48
+ });
49
+ this._applyQuote(writer, blocksToQuote);
50
+ }
51
+ });
52
+ }
53
+ /**
54
+ * Checks the command's {@link #value}.
55
+ */
56
+ _getValue() {
57
+ const selection = this.editor.model.document.selection;
58
+ const firstBlock = first(selection.getSelectedBlocks());
59
+ // In the current implementation, the block quote must be an immediate parent of a block element.
60
+ return !!(firstBlock && findQuote(firstBlock));
61
+ }
62
+ /**
63
+ * Checks whether the command can be enabled in the current context.
64
+ *
65
+ * @returns Whether the command should be enabled.
66
+ */
67
+ _checkEnabled() {
68
+ if (this.value) {
69
+ return true;
70
+ }
71
+ const selection = this.editor.model.document.selection;
72
+ const schema = this.editor.model.schema;
73
+ const firstBlock = first(selection.getSelectedBlocks());
74
+ if (!firstBlock) {
75
+ return false;
76
+ }
77
+ return checkCanBeQuoted(schema, firstBlock);
78
+ }
79
+ /**
80
+ * Removes the quote from given blocks.
81
+ *
82
+ * If blocks which are supposed to be "unquoted" are in the middle of a quote,
83
+ * start it or end it, then the quote will be split (if needed) and the blocks
84
+ * will be moved out of it, so other quoted blocks remained quoted.
85
+ */
86
+ _removeQuote(writer, blocks) {
87
+ // Unquote all groups of block. Iterate in the reverse order to not break following ranges.
88
+ getRangesOfBlockGroups(writer, blocks).reverse().forEach(groupRange => {
89
+ if (groupRange.start.isAtStart && groupRange.end.isAtEnd) {
90
+ writer.unwrap(groupRange.start.parent);
91
+ return;
92
+ }
93
+ // The group of blocks are at the beginning of an <bQ> so let's move them left (out of the <bQ>).
94
+ if (groupRange.start.isAtStart) {
95
+ const positionBefore = writer.createPositionBefore(groupRange.start.parent);
96
+ writer.move(groupRange, positionBefore);
97
+ return;
98
+ }
99
+ // The blocks are in the middle of an <bQ> so we need to split the <bQ> after the last block
100
+ // so we move the items there.
101
+ if (!groupRange.end.isAtEnd) {
102
+ writer.split(groupRange.end);
103
+ }
104
+ // Now we are sure that groupRange.end.isAtEnd is true, so let's move the blocks right.
105
+ const positionAfter = writer.createPositionAfter(groupRange.end.parent);
106
+ writer.move(groupRange, positionAfter);
107
+ });
108
+ }
109
+ /**
110
+ * Applies the quote to given blocks.
111
+ */
112
+ _applyQuote(writer, blocks) {
113
+ const quotesToMerge = [];
114
+ // Quote all groups of block. Iterate in the reverse order to not break following ranges.
115
+ getRangesOfBlockGroups(writer, blocks).reverse().forEach(groupRange => {
116
+ let quote = findQuote(groupRange.start);
117
+ if (!quote) {
118
+ quote = writer.createElement('blockQuote');
119
+ writer.wrap(groupRange, quote);
120
+ }
121
+ quotesToMerge.push(quote);
122
+ });
123
+ // Merge subsequent <bQ> elements. Reverse the order again because this time we want to go through
124
+ // the <bQ> elements in the source order (due to how merge works it moves the right element's content
125
+ // to the first element and removes the right one. Since we may need to merge a couple of subsequent `<bQ>` elements
126
+ // we want to keep the reference to the first (furthest left) one.
127
+ quotesToMerge.reverse().reduce((currentQuote, nextQuote) => {
128
+ if (currentQuote.nextSibling == nextQuote) {
129
+ writer.merge(writer.createPositionAfter(currentQuote));
130
+ return currentQuote;
131
+ }
132
+ return nextQuote;
133
+ });
134
+ }
187
135
  }
188
-
189
- function findQuote( elementOrPosition ) {
190
- return elementOrPosition.parent.name == 'blockQuote' ? elementOrPosition.parent : null;
136
+ function findQuote(elementOrPosition) {
137
+ return elementOrPosition.parent.name == 'blockQuote' ? elementOrPosition.parent : null;
191
138
  }
192
-
193
- // Returns a minimal array of ranges containing groups of subsequent blocks.
194
- //
195
- // content: abcdefgh
196
- // blocks: [ a, b, d, f, g, h ]
197
- // output ranges: [ab]c[d]e[fgh]
198
- //
199
- // @param {Array.<module:engine/model/element~Element>} blocks
200
- // @returns {Array.<module:engine/model/range~Range>}
201
- function getRangesOfBlockGroups( writer, blocks ) {
202
- let startPosition;
203
- let i = 0;
204
- const ranges = [];
205
-
206
- while ( i < blocks.length ) {
207
- const block = blocks[ i ];
208
- const nextBlock = blocks[ i + 1 ];
209
-
210
- if ( !startPosition ) {
211
- startPosition = writer.createPositionBefore( block );
212
- }
213
-
214
- if ( !nextBlock || block.nextSibling != nextBlock ) {
215
- ranges.push( writer.createRange( startPosition, writer.createPositionAfter( block ) ) );
216
- startPosition = null;
217
- }
218
-
219
- i++;
220
- }
221
-
222
- return ranges;
139
+ /**
140
+ * Returns a minimal array of ranges containing groups of subsequent blocks.
141
+ *
142
+ * content: abcdefgh
143
+ * blocks: [ a, b, d, f, g, h ]
144
+ * output ranges: [ab]c[d]e[fgh]
145
+ */
146
+ function getRangesOfBlockGroups(writer, blocks) {
147
+ let startPosition;
148
+ let i = 0;
149
+ const ranges = [];
150
+ while (i < blocks.length) {
151
+ const block = blocks[i];
152
+ const nextBlock = blocks[i + 1];
153
+ if (!startPosition) {
154
+ startPosition = writer.createPositionBefore(block);
155
+ }
156
+ if (!nextBlock || block.nextSibling != nextBlock) {
157
+ ranges.push(writer.createRange(startPosition, writer.createPositionAfter(block)));
158
+ startPosition = null;
159
+ }
160
+ i++;
161
+ }
162
+ return ranges;
223
163
  }
224
-
225
- // Checks whether <bQ> can wrap the block.
226
- function checkCanBeQuoted( schema, block ) {
227
- // TMP will be replaced with schema.checkWrap().
228
- const isBQAllowed = schema.checkChild( block.parent, 'blockQuote' );
229
- const isBlockAllowedInBQ = schema.checkChild( [ '$root', 'blockQuote' ], block );
230
-
231
- return isBQAllowed && isBlockAllowedInBQ;
164
+ /**
165
+ * Checks whether <bQ> can wrap the block.
166
+ */
167
+ function checkCanBeQuoted(schema, block) {
168
+ // TMP will be replaced with schema.checkWrap().
169
+ const isBQAllowed = schema.checkChild(block.parent, 'blockQuote');
170
+ const isBlockAllowedInBQ = schema.checkChild(['$root', 'blockQuote'], block);
171
+ return isBQAllowed && isBlockAllowedInBQ;
232
172
  }
@@ -1,18 +1,14 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module block-quote/blockquoteediting
8
7
  */
9
-
10
8
  import { Plugin } from 'ckeditor5/src/core';
11
9
  import { Enter } from 'ckeditor5/src/enter';
12
10
  import { Delete } from 'ckeditor5/src/typing';
13
-
14
11
  import BlockQuoteCommand from './blockquotecommand';
15
-
16
12
  /**
17
13
  * The block quote editing.
18
14
  *
@@ -21,126 +17,102 @@ import BlockQuoteCommand from './blockquotecommand';
21
17
  * @extends module:core/plugin~Plugin
22
18
  */
23
19
  export default class BlockQuoteEditing extends Plugin {
24
- /**
25
- * @inheritDoc
26
- */
27
- static get pluginName() {
28
- return 'BlockQuoteEditing';
29
- }
30
-
31
- /**
32
- * @inheritDoc
33
- */
34
- static get requires() {
35
- return [ Enter, Delete ];
36
- }
37
-
38
- /**
39
- * @inheritDoc
40
- */
41
- init() {
42
- const editor = this.editor;
43
- const schema = editor.model.schema;
44
-
45
- editor.commands.add( 'blockQuote', new BlockQuoteCommand( editor ) );
46
-
47
- schema.register( 'blockQuote', {
48
- inheritAllFrom: '$container'
49
- } );
50
-
51
- editor.conversion.elementToElement( { model: 'blockQuote', view: 'blockquote' } );
52
-
53
- // Postfixer which cleans incorrect model states connected with block quotes.
54
- editor.model.document.registerPostFixer( writer => {
55
- const changes = editor.model.document.differ.getChanges();
56
-
57
- for ( const entry of changes ) {
58
- if ( entry.type == 'insert' ) {
59
- const element = entry.position.nodeAfter;
60
-
61
- if ( !element ) {
62
- // We are inside a text node.
63
- continue;
64
- }
65
-
66
- if ( element.is( 'element', 'blockQuote' ) && element.isEmpty ) {
67
- // Added an empty blockQuote - remove it.
68
- writer.remove( element );
69
-
70
- return true;
71
- } else if ( element.is( 'element', 'blockQuote' ) && !schema.checkChild( entry.position, element ) ) {
72
- // Added a blockQuote in incorrect place. Unwrap it so the content inside is not lost.
73
- writer.unwrap( element );
74
-
75
- return true;
76
- } else if ( element.is( 'element' ) ) {
77
- // Just added an element. Check that all children meet the scheme rules.
78
- const range = writer.createRangeIn( element );
79
-
80
- for ( const child of range.getItems() ) {
81
- if (
82
- child.is( 'element', 'blockQuote' ) &&
83
- !schema.checkChild( writer.createPositionBefore( child ), child )
84
- ) {
85
- writer.unwrap( child );
86
-
87
- return true;
88
- }
89
- }
90
- }
91
- } else if ( entry.type == 'remove' ) {
92
- const parent = entry.position.parent;
93
-
94
- if ( parent.is( 'element', 'blockQuote' ) && parent.isEmpty ) {
95
- // Something got removed and now blockQuote is empty. Remove the blockQuote as well.
96
- writer.remove( parent );
97
-
98
- return true;
99
- }
100
- }
101
- }
102
-
103
- return false;
104
- } );
105
-
106
- const viewDocument = this.editor.editing.view.document;
107
- const selection = editor.model.document.selection;
108
- const blockQuoteCommand = editor.commands.get( 'blockQuote' );
109
-
110
- // Overwrite default Enter key behavior.
111
- // If Enter key is pressed with selection collapsed in empty block inside a quote, break the quote.
112
- this.listenTo( viewDocument, 'enter', ( evt, data ) => {
113
- if ( !selection.isCollapsed || !blockQuoteCommand.value ) {
114
- return;
115
- }
116
-
117
- const positionParent = selection.getLastPosition().parent;
118
-
119
- if ( positionParent.isEmpty ) {
120
- editor.execute( 'blockQuote' );
121
- editor.editing.view.scrollToTheSelection();
122
-
123
- data.preventDefault();
124
- evt.stop();
125
- }
126
- }, { context: 'blockquote' } );
127
-
128
- // Overwrite default Backspace key behavior.
129
- // If Backspace key is pressed with selection collapsed in first empty block inside a quote, break the quote.
130
- this.listenTo( viewDocument, 'delete', ( evt, data ) => {
131
- if ( data.direction != 'backward' || !selection.isCollapsed || !blockQuoteCommand.value ) {
132
- return;
133
- }
134
-
135
- const positionParent = selection.getLastPosition().parent;
136
-
137
- if ( positionParent.isEmpty && !positionParent.previousSibling ) {
138
- editor.execute( 'blockQuote' );
139
- editor.editing.view.scrollToTheSelection();
140
-
141
- data.preventDefault();
142
- evt.stop();
143
- }
144
- }, { context: 'blockquote' } );
145
- }
20
+ /**
21
+ * @inheritDoc
22
+ */
23
+ static get pluginName() {
24
+ return 'BlockQuoteEditing';
25
+ }
26
+ /**
27
+ * @inheritDoc
28
+ */
29
+ static get requires() {
30
+ return [Enter, Delete];
31
+ }
32
+ /**
33
+ * @inheritDoc
34
+ */
35
+ init() {
36
+ const editor = this.editor;
37
+ const schema = editor.model.schema;
38
+ editor.commands.add('blockQuote', new BlockQuoteCommand(editor));
39
+ schema.register('blockQuote', {
40
+ inheritAllFrom: '$container'
41
+ });
42
+ editor.conversion.elementToElement({ model: 'blockQuote', view: 'blockquote' });
43
+ // Postfixer which cleans incorrect model states connected with block quotes.
44
+ editor.model.document.registerPostFixer(writer => {
45
+ const changes = editor.model.document.differ.getChanges();
46
+ for (const entry of changes) {
47
+ if (entry.type == 'insert') {
48
+ const element = entry.position.nodeAfter;
49
+ if (!element) {
50
+ // We are inside a text node.
51
+ continue;
52
+ }
53
+ if (element.is('element', 'blockQuote') && element.isEmpty) {
54
+ // Added an empty blockQuote - remove it.
55
+ writer.remove(element);
56
+ return true;
57
+ }
58
+ else if (element.is('element', 'blockQuote') && !schema.checkChild(entry.position, element)) {
59
+ // Added a blockQuote in incorrect place. Unwrap it so the content inside is not lost.
60
+ writer.unwrap(element);
61
+ return true;
62
+ }
63
+ else if (element.is('element')) {
64
+ // Just added an element. Check that all children meet the scheme rules.
65
+ const range = writer.createRangeIn(element);
66
+ for (const child of range.getItems()) {
67
+ if (child.is('element', 'blockQuote') &&
68
+ !schema.checkChild(writer.createPositionBefore(child), child)) {
69
+ writer.unwrap(child);
70
+ return true;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ else if (entry.type == 'remove') {
76
+ const parent = entry.position.parent;
77
+ if (parent.is('element', 'blockQuote') && parent.isEmpty) {
78
+ // Something got removed and now blockQuote is empty. Remove the blockQuote as well.
79
+ writer.remove(parent);
80
+ return true;
81
+ }
82
+ }
83
+ }
84
+ return false;
85
+ });
86
+ const viewDocument = this.editor.editing.view.document;
87
+ const selection = editor.model.document.selection;
88
+ const blockQuoteCommand = editor.commands.get('blockQuote');
89
+ // Overwrite default Enter key behavior.
90
+ // If Enter key is pressed with selection collapsed in empty block inside a quote, break the quote.
91
+ this.listenTo(viewDocument, 'enter', (evt, data) => {
92
+ if (!selection.isCollapsed || !blockQuoteCommand.value) {
93
+ return;
94
+ }
95
+ const positionParent = selection.getLastPosition().parent;
96
+ if (positionParent.isEmpty) {
97
+ editor.execute('blockQuote');
98
+ editor.editing.view.scrollToTheSelection();
99
+ data.preventDefault();
100
+ evt.stop();
101
+ }
102
+ }, { context: 'blockquote' });
103
+ // Overwrite default Backspace key behavior.
104
+ // If Backspace key is pressed with selection collapsed in first empty block inside a quote, break the quote.
105
+ this.listenTo(viewDocument, 'delete', (evt, data) => {
106
+ if (data.direction != 'backward' || !selection.isCollapsed || !blockQuoteCommand.value) {
107
+ return;
108
+ }
109
+ const positionParent = selection.getLastPosition().parent;
110
+ if (positionParent.isEmpty && !positionParent.previousSibling) {
111
+ editor.execute('blockQuote');
112
+ editor.editing.view.scrollToTheSelection();
113
+ data.preventDefault();
114
+ evt.stop();
115
+ }
116
+ }, { context: 'blockquote' });
117
+ }
146
118
  }
@@ -1,17 +1,13 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module block-quote/blockquoteui
8
7
  */
9
-
10
8
  import { Plugin, icons } from 'ckeditor5/src/core';
11
9
  import { ButtonView } from 'ckeditor5/src/ui';
12
-
13
10
  import '../theme/blockquote.css';
14
-
15
11
  /**
16
12
  * The block quote UI plugin.
17
13
  *
@@ -20,41 +16,35 @@ import '../theme/blockquote.css';
20
16
  * @extends module:core/plugin~Plugin
21
17
  */
22
18
  export default class BlockQuoteUI extends Plugin {
23
- /**
24
- * @inheritDoc
25
- */
26
- static get pluginName() {
27
- return 'BlockQuoteUI';
28
- }
29
-
30
- /**
31
- * @inheritDoc
32
- */
33
- init() {
34
- const editor = this.editor;
35
- const t = editor.t;
36
-
37
- editor.ui.componentFactory.add( 'blockQuote', locale => {
38
- const command = editor.commands.get( 'blockQuote' );
39
- const buttonView = new ButtonView( locale );
40
-
41
- buttonView.set( {
42
- label: t( 'Block quote' ),
43
- icon: icons.quote,
44
- tooltip: true,
45
- isToggleable: true
46
- } );
47
-
48
- // Bind button model to command.
49
- buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
50
-
51
- // Execute command.
52
- this.listenTo( buttonView, 'execute', () => {
53
- editor.execute( 'blockQuote' );
54
- editor.editing.view.focus();
55
- } );
56
-
57
- return buttonView;
58
- } );
59
- }
19
+ /**
20
+ * @inheritDoc
21
+ */
22
+ static get pluginName() {
23
+ return 'BlockQuoteUI';
24
+ }
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ init() {
29
+ const editor = this.editor;
30
+ const t = editor.t;
31
+ editor.ui.componentFactory.add('blockQuote', locale => {
32
+ const command = editor.commands.get('blockQuote');
33
+ const buttonView = new ButtonView(locale);
34
+ buttonView.set({
35
+ label: t('Block quote'),
36
+ icon: icons.quote,
37
+ tooltip: true,
38
+ isToggleable: true
39
+ });
40
+ // Bind button model to command.
41
+ buttonView.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled');
42
+ // Execute command.
43
+ this.listenTo(buttonView, 'execute', () => {
44
+ editor.execute('blockQuote');
45
+ editor.editing.view.focus();
46
+ });
47
+ return buttonView;
48
+ });
49
+ }
60
50
  }
package/src/index.js CHANGED
@@ -1,12 +1,10 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module block-quote
8
7
  */
9
-
10
8
  export { default as BlockQuote } from './blockquote';
11
9
  export { default as BlockQuoteEditing } from './blockquoteediting';
12
10
  export { default as BlockQuoteUI } from './blockquoteui';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5