annotate .cms/lib/codemirror/src/display/operations.js @ 1:1d486627aa1e draft default tip

24.10
author Coffee CMS <info@coffee-cms.ru>
date Sat, 12 Oct 2024 02:51:39 +0000
parents 78edf6b517a0
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
0
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
1 import { clipPos } from "../line/pos.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
2 import { findMaxLine } from "../line/spans.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
3 import { displayWidth, measureChar, scrollGap } from "../measurement/position_measurement.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
4 import { signal } from "../util/event.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
5 import { activeElt, root } from "../util/dom.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
6 import { finishOperation, pushOperation } from "../util/operation_group.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
7
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
8 import { ensureFocus } from "./focus.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
9 import { measureForScrollbars, updateScrollbars } from "./scrollbars.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
10 import { restartBlink } from "./selection.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
11 import { maybeScrollWindow, scrollPosIntoView, setScrollLeft, setScrollTop } from "./scrolling.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
12 import { DisplayUpdate, maybeClipScrollbars, postUpdateDisplay, setDocumentHeight, updateDisplayIfNeeded } from "./update_display.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
13 import { updateHeightsInViewport } from "./update_lines.js"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
14
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
15 // Operations are used to wrap a series of changes to the editor
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
16 // state in such a way that each change won't have to update the
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
17 // cursor and display (which would be awkward, slow, and
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
18 // error-prone). Instead, display updates are batched and then all
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
19 // combined and executed at once.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
20
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
21 let nextOpId = 0
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
22 // Start a new operation.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
23 export function startOperation(cm) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
24 cm.curOp = {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
25 cm: cm,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
26 viewChanged: false, // Flag that indicates that lines might need to be redrawn
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
27 startHeight: cm.doc.height, // Used to detect need to update scrollbar
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
28 forceUpdate: false, // Used to force a redraw
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
29 updateInput: 0, // Whether to reset the input textarea
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
30 typing: false, // Whether this reset should be careful to leave existing text (for compositing)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
31 changeObjs: null, // Accumulated changes, for firing change events
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
32 cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
33 cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
34 selectionChanged: false, // Whether the selection needs to be redrawn
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
35 updateMaxLine: false, // Set when the widest line needs to be determined anew
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
36 scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
37 scrollToPos: null, // Used to scroll to a specific position
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
38 focus: false,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
39 id: ++nextOpId, // Unique ID
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
40 markArrays: null // Used by addMarkedSpan
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
41 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
42 pushOperation(cm.curOp)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
43 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
44
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
45 // Finish an operation, updating the display and signalling delayed events
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
46 export function endOperation(cm) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
47 let op = cm.curOp
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
48 if (op) finishOperation(op, group => {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
49 for (let i = 0; i < group.ops.length; i++)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
50 group.ops[i].cm.curOp = null
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
51 endOperations(group)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
52 })
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
53 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
54
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
55 // The DOM updates done when an operation finishes are batched so
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
56 // that the minimum number of relayouts are required.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
57 function endOperations(group) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
58 let ops = group.ops
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
59 for (let i = 0; i < ops.length; i++) // Read DOM
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
60 endOperation_R1(ops[i])
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
61 for (let i = 0; i < ops.length; i++) // Write DOM (maybe)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
62 endOperation_W1(ops[i])
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
63 for (let i = 0; i < ops.length; i++) // Read DOM
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
64 endOperation_R2(ops[i])
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
65 for (let i = 0; i < ops.length; i++) // Write DOM (maybe)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
66 endOperation_W2(ops[i])
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
67 for (let i = 0; i < ops.length; i++) // Read DOM
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
68 endOperation_finish(ops[i])
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
69 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
70
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
71 function endOperation_R1(op) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
72 let cm = op.cm, display = cm.display
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
73 maybeClipScrollbars(cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
74 if (op.updateMaxLine) findMaxLine(cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
75
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
76 op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
77 op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
78 op.scrollToPos.to.line >= display.viewTo) ||
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
79 display.maxLineChanged && cm.options.lineWrapping
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
80 op.update = op.mustUpdate &&
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
81 new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
82 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
83
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
84 function endOperation_W1(op) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
85 op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
86 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
87
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
88 function endOperation_R2(op) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
89 let cm = op.cm, display = cm.display
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
90 if (op.updatedDisplay) updateHeightsInViewport(cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
91
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
92 op.barMeasure = measureForScrollbars(cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
93
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
94 // If the max line changed since it was last measured, measure it,
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
95 // and ensure the document's width matches it.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
96 // updateDisplay_W2 will use these properties to do the actual resizing
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
97 if (display.maxLineChanged && !cm.options.lineWrapping) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
98 op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
99 cm.display.sizerWidth = op.adjustWidthTo
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
100 op.barMeasure.scrollWidth =
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
101 Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
102 op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm))
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
103 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
104
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
105 if (op.updatedDisplay || op.selectionChanged)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
106 op.preparedSelection = display.input.prepareSelection()
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
107 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
108
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
109 function endOperation_W2(op) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
110 let cm = op.cm
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
111
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
112 if (op.adjustWidthTo != null) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
113 cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
114 if (op.maxScrollLeft < cm.doc.scrollLeft)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
115 setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
116 cm.display.maxLineChanged = false
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
117 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
118
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
119 let takeFocus = op.focus && op.focus == activeElt(root(cm))
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
120 if (op.preparedSelection)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
121 cm.display.input.showSelection(op.preparedSelection, takeFocus)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
122 if (op.updatedDisplay || op.startHeight != cm.doc.height)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
123 updateScrollbars(cm, op.barMeasure)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
124 if (op.updatedDisplay)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
125 setDocumentHeight(cm, op.barMeasure)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
126
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
127 if (op.selectionChanged) restartBlink(cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
128
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
129 if (cm.state.focused && op.updateInput)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
130 cm.display.input.reset(op.typing)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
131 if (takeFocus) ensureFocus(op.cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
132 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
133
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
134 function endOperation_finish(op) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
135 let cm = op.cm, display = cm.display, doc = cm.doc
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
136
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
137 if (op.updatedDisplay) postUpdateDisplay(cm, op.update)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
138
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
139 // Abort mouse wheel delta measurement, when scrolling explicitly
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
140 if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
141 display.wheelStartX = display.wheelStartY = null
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
142
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
143 // Propagate the scroll position to the actual DOM scroller
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
144 if (op.scrollTop != null) setScrollTop(cm, op.scrollTop, op.forceScroll)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
145
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
146 if (op.scrollLeft != null) setScrollLeft(cm, op.scrollLeft, true, true)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
147 // If we need to scroll a specific position into view, do so.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
148 if (op.scrollToPos) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
149 let rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
150 clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
151 maybeScrollWindow(cm, rect)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
152 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
153
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
154 // Fire events for markers that are hidden/unidden by editing or
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
155 // undoing
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
156 let hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
157 if (hidden) for (let i = 0; i < hidden.length; ++i)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
158 if (!hidden[i].lines.length) signal(hidden[i], "hide")
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
159 if (unhidden) for (let i = 0; i < unhidden.length; ++i)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
160 if (unhidden[i].lines.length) signal(unhidden[i], "unhide")
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
161
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
162 if (display.wrapper.offsetHeight)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
163 doc.scrollTop = cm.display.scroller.scrollTop
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
164
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
165 // Fire change events, and delayed event handlers
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
166 if (op.changeObjs)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
167 signal(cm, "changes", cm, op.changeObjs)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
168 if (op.update)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
169 op.update.finish()
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
170 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
171
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
172 // Run the given function in an operation
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
173 export function runInOp(cm, f) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
174 if (cm.curOp) return f()
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
175 startOperation(cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
176 try { return f() }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
177 finally { endOperation(cm) }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
178 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
179 // Wraps a function in an operation. Returns the wrapped function.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
180 export function operation(cm, f) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
181 return function() {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
182 if (cm.curOp) return f.apply(cm, arguments)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
183 startOperation(cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
184 try { return f.apply(cm, arguments) }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
185 finally { endOperation(cm) }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
186 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
187 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
188 // Used to add methods to editor and doc instances, wrapping them in
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
189 // operations.
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
190 export function methodOp(f) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
191 return function() {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
192 if (this.curOp) return f.apply(this, arguments)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
193 startOperation(this)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
194 try { return f.apply(this, arguments) }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
195 finally { endOperation(this) }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
196 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
197 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
198 export function docMethodOp(f) {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
199 return function() {
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
200 let cm = this.cm
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
201 if (!cm || cm.curOp) return f.apply(this, arguments)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
202 startOperation(cm)
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
203 try { return f.apply(this, arguments) }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
204 finally { endOperation(cm) }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
205 }
Coffee CMS <info@coffee-cms.ru>
parents:
diff changeset
206 }