|
| 1 | +# Locations |
| 2 | + |
| 3 | +Before understanding what locations are and how they are helpful, we must first understand the structure of a BlockNote document by it's parts: |
| 4 | + |
| 5 | +```ts |
| 6 | +type Text = { |
| 7 | + type: "text"; |
| 8 | + text: string; |
| 9 | + styles?: Object; |
| 10 | +}; |
| 11 | + |
| 12 | +type CustomInlineContent = { |
| 13 | + type: string; |
| 14 | + props: Object; |
| 15 | + content: Text[] | undefined; |
| 16 | +}; |
| 17 | + |
| 18 | +type Block = { |
| 19 | + id: string; |
| 20 | + type: string; |
| 21 | + props: Object; |
| 22 | + content: (Text | CustomInlineContent)[] | undefined; |
| 23 | + children: Block[] | undefined; |
| 24 | +}; |
| 25 | + |
| 26 | +type Document = Block[]; |
| 27 | +``` |
| 28 | + |
| 29 | +So, as you can see, the editor's content can be quite layered and nested. |
| 30 | + |
| 31 | +- A document contains blocks |
| 32 | +- A block may contain text, or inline content, or be empty altogether |
| 33 | + - A block may also have child blocks (which are visually indented) |
| 34 | +- inline content may contain text or be empty altogether |
| 35 | + |
| 36 | +Blocks have identifiers, but inline content & text do not have unique identifiers, so it can be tricky to describe their position within the document. Usually this is done relative to the current text cursor, but there needs to be a better way about this than moving the selection around by offsets. |
| 37 | + |
| 38 | +## Enter Locations |
| 39 | + |
| 40 | +A location is meant to be a generic way of describing a position within a document. It is flexible enough to describe a single position within the document (e.g. in-between two letters anywhere within the document) as well as be flexible enough to represent ranges of positions across the document (e.g. the current selected text). This can occur at different levels of specificity, sometimes you care only about block-level operations (e.g. when updating a block's props), other times you care about character level operations (e.g. inserting a character into the document at a specific position), and still other times you care about ranges of positions (e.g. marking a sentence to be bold). |
| 41 | + |
| 42 | +Locations provide this flexibility by being flexible in their definition like below: |
| 43 | + |
| 44 | +- **BlockId**: block level specificity |
| 45 | + - Technically just a `Point` where the offset is `-1` |
| 46 | +- **Point**: character level specificity |
| 47 | + - a pair of a `BlockId` & `offset` |
| 48 | +- **Range**: a range of items at character level specificity |
| 49 | + - two points `anchor` & `head` |
| 50 | +- **BlockRange**: a range of blocks at block level specificity |
| 51 | + - Technically just two `Range`s, with both of their offsets as `-1` |
| 52 | + |
| 53 | +A `Location` is defined to be any of `BlockId | Point | Range` allowing for varying levels of specificity. In some cases, operations may only operate at the block-level, so passing something of higher specificity will only operate at that level. In other cases, an operation may require a higher-level of specificity and therefore will be required to declare that at the type-level. |
| 54 | + |
| 55 | +## Example |
| 56 | + |
| 57 | +So, that was a lot of explanation, let's see an example to make this concrete. We will start by defining a document like so: |
| 58 | + |
| 59 | +```ts |
| 60 | +const document = [ |
| 61 | + { |
| 62 | + id: "first-paragraph", |
| 63 | + type: "paragraph", |
| 64 | + props: {}, |
| 65 | + content: [ |
| 66 | + { |
| 67 | + type: "text", |
| 68 | + text: "abc", |
| 69 | + }, |
| 70 | + ], |
| 71 | + }, |
| 72 | + { |
| 73 | + id: "second-paragraph", |
| 74 | + type: "paragraph", |
| 75 | + props: {}, |
| 76 | + content: [ |
| 77 | + { |
| 78 | + type: "text", |
| 79 | + text: "def", |
| 80 | + }, |
| 81 | + ], |
| 82 | + }, |
| 83 | +]; |
| 84 | +``` |
| 85 | + |
| 86 | +Based on this document, we can see different ways of declaring the positions in this document: |
| 87 | + |
| 88 | +```ts |
| 89 | +// selects the whole first paragraph |
| 90 | +const a = "first-paragraph" satisfies BlockId; |
| 91 | + |
| 92 | +const b = { id: "first-paragraph" } satisfies BlockIdentifier; |
| 93 | + |
| 94 | +const c = { id: "first-paragraph", offset: -1 } satisfies Point; |
| 95 | + |
| 96 | +const d = { |
| 97 | + anchor: { id: "first-paragraph", offset: -1 }, |
| 98 | + head: { id: "first-paragraph", offset: -1 }, |
| 99 | +} satisfies Range; |
| 100 | + |
| 101 | +const e = ["first-paragraph", "first-paragraph"] satisfies BlockRange; |
| 102 | + |
| 103 | +// any of the above satisfies as a `Location` |
| 104 | +const f = (a || b || c || d || e) satisfies Location; |
| 105 | +``` |
| 106 | + |
| 107 | +Whereas, if we want to select the position starting at character 'b' then only these are valid: |
| 108 | + |
| 109 | +```ts |
| 110 | +const g = { id: "first-paragraph", offset: 1 } satisfies Point; |
| 111 | + |
| 112 | +const h = { |
| 113 | + anchor: { id: "first-paragraph", offset: 1 }, |
| 114 | + head: { id: "first-paragraph", offset: 1 }, |
| 115 | +} satisfies Range; |
| 116 | +``` |
| 117 | + |
| 118 | +Or, some other examples: |
| 119 | + |
| 120 | +```ts |
| 121 | +// select from character 'b' to character 'c' |
| 122 | +const i = { |
| 123 | + anchor: { id: "first-paragraph", offset: 1 }, |
| 124 | + head: { id: "first-paragraph", offset: 2 }, |
| 125 | +} satisfies Range; |
| 126 | +// select from character 'b' to character 'e' |
| 127 | +const i = { |
| 128 | + anchor: { id: "first-paragraph", offset: 1 }, |
| 129 | + head: { id: "second-paragraph", offset: 1 }, |
| 130 | +} satisfies Range; |
| 131 | +// select from first paragraph to the second paragraph |
| 132 | +const j = { |
| 133 | + anchor: { id: "first-paragraph", offset: -1 }, |
| 134 | + head: { id: "second-paragraph", offset: -1 }, |
| 135 | +} satisfies Range; |
| 136 | +const k = ["first-paragraph", "second-paragraph"] satisfies BlockRange; |
| 137 | +``` |
| 138 | + |
| 139 | +## Nested content |
| 140 | + |
| 141 | +To interpret nested content, you can imagine Ranges as being inclusive of their children, or exclusive of their children. For example, when out-denting a block with children, you would want that operation to be exclusive of it's children (i.e. only out-dent the parents, not their children), whereas bolding would be inclusive of it's children (i.e. bold should apply to the parent & children selected). As such, this is dependent on the type of operation, not of the `Location` being specified, so methods will need to be clear of their expectations upfront. By default, operations will be **assumed to be _inclusive_ of children**. |
0 commit comments