Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#6432: Introduced SchemaItemDefinition#isSelectable and SchemaItemDefinition#isContent. #7770

Merged
merged 17 commits into from
Aug 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
edb40fc
WIP
oleq Jul 15, 2020
2ac4ea5
Merge branch 'master' into i/6432-schema-is-selectable
oleq Jul 30, 2020
a8ce29d
Added the Schema#isContent() method. Made $text a content in schema.
oleq Jul 30, 2020
dac42e8
Tests: Added Schema#isContent() and Schema#isSelectable() method tests.
oleq Aug 3, 2020
1453b7f
Tests: Extended a model class test to verify that $text is registered…
oleq Aug 3, 2020
e1f9207
Updated the model selection post-fixer to properly handle table cells…
oleq Aug 3, 2020
0892e08
Tests: Updated TableEditing tests to reflect the new table and tableC…
oleq Aug 3, 2020
b02783e
Updated the Model#hasContent() method implementation to use Schema#is…
oleq Aug 3, 2020
72f5036
Tests: Updated the tableCell element schema definitions since it is n…
oleq Aug 3, 2020
b9d7516
Docs: Added sections about selectable elements and content elements t…
oleq Aug 3, 2020
986c969
Docs: Added a table of schema properties to the schema deep dive guide.
oleq Aug 3, 2020
e12c280
Docs: Added #isSelectable and #isContent to SchemaItemDefinition.
oleq Aug 3, 2020
9f3783a
BalloonToolbar should not show up when multiple table cells are selec…
oleq Aug 4, 2020
a7d6fe2
Tests: Added a Model#hasContent integration tests for an empty select…
oleq Aug 4, 2020
4126970
Updated the modifySelection() helper to consider elements defined as …
oleq Aug 4, 2020
518f1d3
Updated the model selection post-fixer so it does not fix the selecti…
oleq Aug 4, 2020
f6b0b4d
Made it possible to create a Schema object out of three granular prop…
oleq Aug 4, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 304 additions & 3 deletions packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
category: framework-deep-dive
classes: schema-deep-dive
---

# Schema
Expand Down Expand Up @@ -48,6 +49,222 @@ While this would be incorrect:

In addition to setting allowed structures, the schema can also define additional traits of model elements. By using the `is*` properties, a feature author may declare how a certain element should be treated by other features and the engine.

Here is a table listing various model elements and their properties registered in the schema:

<table>
<thead>
<tr>
<th rowspan="2">Schema entry</th>
<th colspan="6">Properties in the <a href="#defining-allowed-structures">definition</a></th>
</tr>
<tr>
<th><a href="#block-elements"><code>isBlock</code></a></th>
<th><a href="#limit-elements"><code>isLimit</code></a></th>
<th><a href="#object-elements"><code>isObject</code></a></th>
<th><a href="#inline-elements"><code>isInline</code></a></th>
<th><a href="#selectable-elements"><code>isSelectable</code></a></th>
<th><a href="#content-elements"><code>isContent</code></a></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>$block</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>$clipboardHolder</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>$marker</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>$root</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>$text</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
</tr>
<tr>
<td><code>blockQuote</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>caption</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>codeBlock</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>heading1</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>heading2</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>heading3</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>horizontalLine</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
</tr>
<tr>
<td><code>image</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
</tr>
<tr>
<td><code>listItem</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>media</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
</tr>
<tr>
<td><code>pageBreak</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
</tr>
<tr>
<td><code>paragraph</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>softBreak</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>table</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited1"><sup>[1]</sup></a></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited2"><sup>[2]</sup></a></td>
<td class="value_positive_inherited"><code>true</code><a href="#inherited3"><sup>[3]</sup></a></td>
</tr>
<tr>
<td><code>tableRow</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
<tr>
<td><code>tableCell</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_negative"><code>false</code></td>
<td class="value_positive"><code>true</code></td>
<td class="value_negative"><code>false</code></td>
</tr>
</tbody>
</table>

<info-box>
* <span id="inherited1">[1]</span> The value of `isLimit` is `true` for this element because all [objects](#object-elements) are automatically [limit elements](#limit-elements),
* <span id="inherited2">[2]</span> The value of `isSelectable` is `true` for this element because all [objects](#object-elements) are automatically [selectable elements](#selectable-elements),
* <span id="inherited3">[3]</span> The value of `isContent` is `true` for this element because all [objects](#object-elements) are automatically [content elements](#content-elements).
</info-box>

### Limit elements

Consider a feature like an image caption. The caption text area should construct a boundary to some internal actions:
Expand Down Expand Up @@ -86,9 +303,11 @@ schema.register( 'myImage', {
The {@link module:engine/model/schema~Schema#isObject `Schema#isObject()`} can later be used to check this property.

<info-box>
Every "object" is also a "limit" element.
Every "object" is automatically also:

It means that for every element with `isObject` set to `true`, {@link module:engine/model/schema~Schema#isLimit `Schema#isLimit( element )`} will always return `true`.
* a [limit element](#limit-elements) – for every element with `isObject` set `true`, {@link module:engine/model/schema~Schema#isLimit `Schema#isLimit( element )`} will always return `true`.
* a [selectable element](#selectable-elements) – for every element with `isObject` set `true`, {@link module:engine/model/schema~Schema#isSelectable `Schema#isSelectable( element )`} will always return `true`.
* a [content element](#content-elements) – for every element with `isObject` set `true`, {@link module:engine/model/schema~Schema#isContent `Schema#isContent( element )`} will always return `true`.
</info-box>

### Block elements
Expand All @@ -109,6 +328,42 @@ Currently, the {@link module:engine/model/schema~SchemaItemDefinition#isInline `

The support for inline elements in CKEditor 5 is so far limited to self-contained elements. Because of this, all elements marked with `isInline` should also be marked with `isObject`.

### Selectable elements

Elements that users can select as a whole (with all their internals) and then, for instance, copy them or apply formatting, are marked with the {@link module:engine/model/schema~SchemaItemDefinition#isSelectable `isSelectable`} property in the schema:

```js
schema.register( 'mySelectable', {
isSelectable: true
} );
```

The {@link module:engine/model/schema~Schema#isSelectable `Schema#isSelectable()`} method can later be used to check this property.

<info-box>
All [object elements](#object-elements) are selectable by default. There are other selectable elements registered in the editor, though. For instance, there is also the `tableCell` model element (rendered as a `<td>` in the editing view) that is selectable while **not** registered as an object. The {@link features/table#table-selection table selection} plugin takes advantage of this fact and allows users create rectangular selections made of multiple table cells.
</info-box>

### Content elements

You can tell content model elements from other elements by looking at their representation in the editor data (you can use {@link module:editor-classic/classiceditor~ClassicEditor#getData `editor.getData()`} or {@link module:engine/model/model~Model#hasContent Model#hasContent()} to check this out).

Elements such as images or media will **always** find their way into editor data and this is what makes them content elements. They are marked with the {@link module:engine/model/schema~SchemaItemDefinition#isContent `isContent`} property in the schema:

```js
schema.register( 'myImage', {
isContent: true
} );
```

The {@link module:engine/model/schema~Schema#isContent `Schema#isContent()`} method can later be used to check this property.

At the same time, elements like paragraphs, list items, or headings **are not** content elements because they are skipped in the editor output when they are empty. From the data perspective they are transparent unless they contain other content elements (an empty paragraph is as good as no paragraph).

<info-box>
[Object elements](#object-elements) and [`$text`](#generic-items) are content by default.
</info-box>

## Generic items

There are three basic generic items: `$root`, `$block` and `$text`. They are defined as follows:
Expand Down Expand Up @@ -267,4 +522,50 @@ Finally, the schema plays a crucial role during the conversion from the view to
Some features may miss schema checks. If you happen to find such a scenario, do not hesitate to [report it to us](https://github.com/ckeditor/ckeditor5/issues).
</info-box>


<style>
.schema-deep-dive table {
text-align: center;
}

.schema-deep-dive table td,
.schema-deep-dive table th {
border-color: hsl(72deg 6% 16%);
}

.schema-deep-dive table thead th {
font-weight: bold;
vertical-align: middle;
}

.schema-deep-dive table thead th code {
white-space: nowrap;
}

.schema-deep-dive table tbody td.value_negative {
background: hsl(354deg, 100%, 90%);
}

.schema-deep-dive table tbody td.value_positive {
background: hsl(88deg, 50%, 60%);
}

.schema-deep-dive table tbody td.value_negative code,
.schema-deep-dive table tbody td.value_positive code,
.schema-deep-dive table tbody td.value_positive_inherited code {
background: none;
text-shadow: 0px 0px 2px hsl(0deg, 0%, 100%);
}

.schema-deep-dive table tbody td.value_positive_inherited {
background-image: linear-gradient(45deg, hsl(88deg, 50%, 60%) 25%, hsl(89deg, 58% ,71%) 25%, hsl(89deg, 58%, 71%) 50%, hsl(88deg, 50%, 60%) 50%, hsl(88deg, 50%, 60%) 75%, hsl(89deg, 58%, 71%) 75%, hsl(89deg, 58%, 71%) 100%);
background-size: 3px 3px;
}

.schema-deep-dive table tbody td sup {
top: -0.5em;
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
</style>
19 changes: 11 additions & 8 deletions packages/ckeditor5-engine/src/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ export default class Model {
} );
this.schema.register( '$text', {
allowIn: '$block',
isInline: true
isInline: true,
isContent: true
} );
this.schema.register( '$clipboardHolder', {
allowContentOf: '$root',
Expand Down Expand Up @@ -540,7 +541,7 @@ export default class Model {
*
* * any text node (`options.ignoreWhitespaces` allows controlling whether this text node must also contain
* any non-whitespace characters),
* * or any {@link module:engine/model/schema~Schema#isObject object element},
* * or any {@link module:engine/model/schema~Schema#isContent content element},
* * or any {@link module:engine/model/markercollection~Marker marker} which
* {@link module:engine/model/markercollection~Marker#_affectsData affects data}.
*
Expand Down Expand Up @@ -573,14 +574,16 @@ export default class Model {
}

for ( const item of range.getItems() ) {
if ( item.is( '$textProxy' ) ) {
if ( !ignoreWhitespaces ) {
return true;
} else if ( item.data.search( /\S/ ) !== -1 ) {
if ( this.schema.isContent( item ) ) {
if ( item.is( '$textProxy' ) ) {
if ( !ignoreWhitespaces ) {
return true;
} else if ( item.data.search( /\S/ ) !== -1 ) {
return true;
}
} else {
return true;
}
} else if ( this.schema.isObject( item ) ) {
return true;
}
}

Expand Down
Loading