1 /*
2 * This file is part of the KDE libraries
3 * Copyright (C) 2001 Peter Kelly (pmk@post.com)
4 * Copyright (C) 2003 Apple Computer, Inc.
5 * Copyright (C) 2009 Maksim Orlovich (maksim@kde.org)
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Library General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Library General Public License for more details.
16 *
17 * You should have received a copy of the GNU Library General Public
18 * License along with this library; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 */
21
22 #include "kjs_range.h"
23 #include "kjs_range.lut.h"
24 #include "khtml_part.h"
25 #include "dom/dom_exception.h"
26 #include "dom/dom2_range.h"
27 #include "khtml_debug.h"
28
29 using DOM::DOMException;
30
31 namespace KJS
32 {
33 // -------------------------------------------------------------------------
34
35 const ClassInfo DOMRange::info = { "Range", nullptr, &DOMRangeTable, nullptr };
36 /*
37 @begin DOMRangeTable 7
38 startContainer DOMRange::StartContainer DontDelete|ReadOnly
39 startOffset DOMRange::StartOffset DontDelete|ReadOnly
40 endContainer DOMRange::EndContainer DontDelete|ReadOnly
41 endOffset DOMRange::EndOffset DontDelete|ReadOnly
42 collapsed DOMRange::Collapsed DontDelete|ReadOnly
43 commonAncestorContainer DOMRange::CommonAncestorContainer DontDelete|ReadOnly
44 @end
45 @begin DOMRangeProtoTable 17
46 setStart DOMRange::SetStart DontDelete|Function 2
47 setEnd DOMRange::SetEnd DontDelete|Function 2
48 setStartBefore DOMRange::SetStartBefore DontDelete|Function 1
49 setStartAfter DOMRange::SetStartAfter DontDelete|Function 1
50 setEndBefore DOMRange::SetEndBefore DontDelete|Function 1
51 setEndAfter DOMRange::SetEndAfter DontDelete|Function 1
52 collapse DOMRange::Collapse DontDelete|Function 1
53 selectNode DOMRange::SelectNode DontDelete|Function 1
54 selectNodeContents DOMRange::SelectNodeContents DontDelete|Function 1
55 compareBoundaryPoints DOMRange::CompareBoundaryPoints DontDelete|Function 2
56 deleteContents DOMRange::DeleteContents DontDelete|Function 0
57 extractContents DOMRange::ExtractContents DontDelete|Function 0
58 cloneContents DOMRange::CloneContents DontDelete|Function 0
59 insertNode DOMRange::InsertNode DontDelete|Function 1
60 surroundContents DOMRange::SurroundContents DontDelete|Function 1
61 cloneRange DOMRange::CloneRange DontDelete|Function 0
62 toString DOMRange::ToString DontDelete|Function 0
63 detach DOMRange::Detach DontDelete|Function 0
64 createContextualFragment DOMRange::CreateContextualFragment DontDelete|Function 1
65 @end
66 */
67 KJS_DEFINE_PROTOTYPE(DOMRangeProto)
KJS_IMPLEMENT_PROTOFUNC(DOMRangeProtoFunc)68 KJS_IMPLEMENT_PROTOFUNC(DOMRangeProtoFunc)
69 KJS_IMPLEMENT_PROTOTYPE("DOMRange", DOMRangeProto, DOMRangeProtoFunc, ObjectPrototype)
70
71 DOMRange::DOMRange(ExecState *exec, DOM::RangeImpl *r)
72 : m_impl(r)
73 {
74 assert(r);
75 setPrototype(DOMRangeProto::self(exec));
76 }
77
~DOMRange()78 DOMRange::~DOMRange()
79 {
80 ScriptInterpreter::forgetDOMObject(m_impl.get());
81 }
82
getOwnPropertySlot(ExecState * exec,const Identifier & propertyName,PropertySlot & slot)83 bool DOMRange::getOwnPropertySlot(ExecState *exec, const Identifier &propertyName, PropertySlot &slot)
84 {
85 return getStaticValueSlot<DOMRange, DOMObject>(exec, &DOMRangeTable, this, propertyName, slot);
86 }
87
getValueProperty(ExecState * exec,int token) const88 JSValue *DOMRange::getValueProperty(ExecState *exec, int token) const
89 {
90 DOMExceptionTranslator exception(exec);
91 DOM::RangeImpl &range = *m_impl;
92
93 switch (token) {
94 case StartContainer:
95 return getDOMNode(exec, range.startContainer(exception));
96 case StartOffset:
97 return jsNumber(range.startOffset(exception));
98 case EndContainer:
99 return getDOMNode(exec, range.endContainer(exception));
100 case EndOffset:
101 return jsNumber(range.endOffset(exception));
102 case Collapsed:
103 return jsBoolean(range.collapsed(exception));
104 case CommonAncestorContainer: {
105 return getDOMNode(exec, range.commonAncestorContainer(exception));
106 }
107 default:
108 // qCDebug(KHTML_LOG) << "WARNING: Unhandled token in DOMRange::getValueProperty : " << token;
109 return jsNull();
110 }
111 }
112
callAsFunction(ExecState * exec,JSObject * thisObj,const List & args)113 JSValue *DOMRangeProtoFunc::callAsFunction(ExecState *exec, JSObject *thisObj, const List &args)
114 {
115 KJS_CHECK_THIS(KJS::DOMRange, thisObj);
116 DOMExceptionTranslator exception(exec);
117 DOM::RangeImpl &range = *static_cast<DOMRange *>(thisObj)->impl();
118
119 JSValue *result = jsUndefined();
120
121 switch (id) {
122 case DOMRange::SetStart:
123 range.setStart(toNode(args[0]), args[1]->toInteger(exec), exception);
124 break;
125 case DOMRange::SetEnd:
126 range.setEnd(toNode(args[0]), args[1]->toInteger(exec), exception);
127 break;
128 case DOMRange::SetStartBefore:
129 range.setStartBefore(toNode(args[0]), exception);
130 break;
131 case DOMRange::SetStartAfter:
132 range.setStartAfter(toNode(args[0]), exception);
133 break;
134 case DOMRange::SetEndBefore:
135 range.setEndBefore(toNode(args[0]), exception);
136 break;
137 case DOMRange::SetEndAfter:
138 range.setEndAfter(toNode(args[0]), exception);
139 break;
140 case DOMRange::Collapse:
141 range.collapse(args[0]->toBoolean(exec), exception);
142 break;
143 case DOMRange::SelectNode:
144 range.selectNode(toNode(args[0]), exception);
145 break;
146 case DOMRange::SelectNodeContents:
147 range.selectNodeContents(toNode(args[0]), exception);
148 break;
149 case DOMRange::CompareBoundaryPoints:
150 result = jsNumber(range.compareBoundaryPoints(static_cast<DOM::Range::CompareHow>(args[0]->toInt32(exec)), toRange(args[1]), exception));
151 break;
152 case DOMRange::DeleteContents:
153 range.deleteContents(exception);
154 break;
155 case DOMRange::ExtractContents:
156 result = getDOMNode(exec, range.extractContents(exception));
157 break;
158 case DOMRange::CloneContents:
159 result = getDOMNode(exec, range.cloneContents(exception));
160 break;
161 case DOMRange::InsertNode:
162 range.insertNode(toNode(args[0]), exception);
163 break;
164 case DOMRange::SurroundContents:
165 range.surroundContents(toNode(args[0]), exception);
166 break;
167 case DOMRange::CloneRange:
168 result = getDOMRange(exec, range.cloneRange(exception));
169 break;
170 case DOMRange::ToString:
171 result = jsString(UString(range.toString(exception)));
172 break;
173 case DOMRange::Detach:
174 range.detach(exception);
175 break;
176 case DOMRange::CreateContextualFragment:
177 JSValue *value = args[0];
178 DOM::DOMString str = value->type() == NullType ? DOM::DOMString() : value->toString(exec).domString();
179 DOM::DocumentFragment frag = range.createContextualFragment(str, exception);
180 result = getDOMNode(exec, frag.handle());
181 break;
182 };
183
184 return result;
185 }
186
getDOMRange(ExecState * exec,DOM::RangeImpl * r)187 JSValue *getDOMRange(ExecState *exec, DOM::RangeImpl *r)
188 {
189 return cacheDOMObject<DOM::RangeImpl, KJS::DOMRange>(exec, r);
190 }
191
192 // -------------------------------------------------------------------------
193
194 const ClassInfo RangeConstructor::info = { "RangeConstructor", nullptr, &RangeConstructorTable, nullptr };
195 /*
196 @begin RangeConstructorTable 5
197 START_TO_START DOM::Range::START_TO_START DontDelete|ReadOnly
198 START_TO_END DOM::Range::START_TO_END DontDelete|ReadOnly
199 END_TO_END DOM::Range::END_TO_END DontDelete|ReadOnly
200 END_TO_START DOM::Range::END_TO_START DontDelete|ReadOnly
201 @end
202 */
203
RangeConstructor(ExecState * exec)204 RangeConstructor::RangeConstructor(ExecState *exec)
205 : DOMObject(exec->lexicalInterpreter()->builtinObjectPrototype())
206 {
207 putDirect(exec->propertyNames().prototype, DOMRangeProto::self(exec), DontEnum | DontDelete | ReadOnly);
208 }
209
getOwnPropertySlot(ExecState * exec,const Identifier & propertyName,PropertySlot & slot)210 bool RangeConstructor::getOwnPropertySlot(ExecState *exec, const Identifier &propertyName, PropertySlot &slot)
211 {
212 return getStaticValueSlot<RangeConstructor, DOMObject>(exec, &RangeConstructorTable, this, propertyName, slot);
213 }
214
getValueProperty(ExecState *,int token) const215 JSValue *RangeConstructor::getValueProperty(ExecState *, int token) const
216 {
217 return jsNumber(token);
218 }
219
getRangeConstructor(ExecState * exec)220 JSValue *getRangeConstructor(ExecState *exec)
221 {
222 return cacheGlobalObject<RangeConstructor>(exec, "[[range.constructor]]");
223 }
224
toRange(JSValue * val)225 DOM::RangeImpl *toRange(JSValue *val)
226 {
227 JSObject *obj = val->getObject();
228 if (!obj || !obj->inherits(&DOMRange::info)) {
229 return nullptr;
230 }
231
232 const DOMRange *dobj = static_cast<const DOMRange *>(obj);
233 return dobj->impl();
234 }
235
236 /* Source for RangeExceptionProtoTable.
237 @begin RangeExceptionProtoTable 2
238 BAD_BOUNDARYPOINTS_ERR DOM::RangeException::BAD_BOUNDARYPOINTS_ERR DontDelete|ReadOnly
239 INVALID_NODE_TYPE_ERR DOM::RangeException::INVALID_NODE_TYPE_ERR DontDelete|ReadOnly
240 @end
241 */
242
243 DEFINE_CONSTANT_TABLE(RangeExceptionProto)
244 IMPLEMENT_CONSTANT_TABLE(RangeExceptionProto, "RangeException")
245
246 IMPLEMENT_PSEUDO_CONSTRUCTOR_WITH_PARENT(RangeExceptionPseudoCtor, "RangeException",
247 RangeExceptionProto, RangeExceptionProto)
248
RangeException(ExecState * exec)249 RangeException::RangeException(ExecState *exec)
250 : DOMObject(RangeExceptionProto::self(exec))
251 {
252 }
253
254 const ClassInfo RangeException::info = { "RangeException", nullptr, nullptr, nullptr };
255
256 // -------------------------------------------------------------------------
257
258 const ClassInfo DOMSelection::info = { "Selection", nullptr, &DOMSelectionTable, nullptr };
259 /*
260 @begin DOMSelectionTable 7
261 anchorNode DOMSelection::AnchorNode DontDelete|ReadOnly
262 anchorOffset DOMSelection::AnchorOffset DontDelete|ReadOnly
263 focusNode DOMSelection::FocusNode DontDelete|ReadOnly
264 focusOffset DOMSelection::FocusOffset DontDelete|ReadOnly
265 isCollapsed DOMSelection::IsCollapsed DontDelete|ReadOnly
266 rangeCount DOMSelection::RangeCount DontDelete|ReadOnly
267 @end
268 @begin DOMSelectionProtoTable 13
269 collapsed DOMSelection::Collapsed DontDelete|Function 2
270 collapseToStart DOMSelection::CollapseToStart DontDelete|Function 0
271 collapseToEnd DOMSelection::CollapseToEnd DontDelete|Function 0
272 selectAllChildren DOMSelection::SelectAllChildren DontDelete|Function 1
273 deleteFromDocument DOMSelection::DeleteFromDocument DontDelete|Function 0
274 getRangeAt DOMSelection::GetRangeAt DontDelete|Function 1
275 addRange DOMSelection::AddRange DontDelete|Function 1
276 removeRange DOMSelection::RemoveRange DontDelete|Function 1
277 removeAllRanges DOMSelection::RemoveAllRanges DontDelete|Function 0
278 toString DOMSelection::ToString DontDelete|Function 0
279 @end
280 */
281 KJS_DEFINE_PROTOTYPE(DOMSelectionProto)
KJS_IMPLEMENT_PROTOFUNC(DOMSelectionProtoFunc)282 KJS_IMPLEMENT_PROTOFUNC(DOMSelectionProtoFunc)
283 KJS_IMPLEMENT_PROTOTYPE("Selection", DOMSelectionProto, DOMSelectionProtoFunc, ObjectPrototype)
284
285 DOMSelection::DOMSelection(ExecState *exec, DOM::DocumentImpl *parentDocument):
286 JSObject(DOMSelectionProto::self(exec)), m_document(parentDocument)
287 {}
288
getOwnPropertySlot(ExecState * exec,const Identifier & propertyName,PropertySlot & slot)289 bool DOMSelection::getOwnPropertySlot(ExecState *exec, const Identifier &propertyName, PropertySlot &slot)
290 {
291 // qCDebug(KHTML_LOG) << propertyName.ustring().qstring();
292 return getStaticValueSlot<DOMSelection, JSObject>(exec, &DOMSelectionTable, this, propertyName, slot);
293 }
294
getValueProperty(ExecState * exec,int token) const295 JSValue *DOMSelection::getValueProperty(ExecState *exec, int token) const
296 {
297 // qCDebug(KHTML_LOG) << token;
298 DOMExceptionTranslator exception(exec);
299 DOM::Selection sel = currentSelection();
300 // ### TODO: below doesn't really distinguish anchor and focus properly.
301 switch (token) {
302 case DOMSelection::AnchorNode:
303 return sel.notEmpty() ? getDOMNode(exec, sel.base().node()) : jsNull();
304 case DOMSelection::AnchorOffset:
305 return jsNumber(sel.notEmpty() ? sel.base().offset() : 0L);
306 case DOMSelection::FocusNode:
307 return sel.notEmpty() ? getDOMNode(exec, sel.extent().node()) : jsNull();
308 case DOMSelection::FocusOffset:
309 return jsNumber(sel.notEmpty() ? sel.extent().offset() : 0L);
310 case DOMSelection::IsCollapsed:
311 return jsBoolean(sel.isCollapsed() || sel.isEmpty());
312 case DOMSelection::RangeCount:
313 return sel.notEmpty() ? jsNumber(1) : jsNumber(0);
314 }
315
316 assert(false);
317 return jsUndefined();
318 }
319
callAsFunction(ExecState * exec,JSObject * thisObj,const List & args)320 JSValue *DOMSelectionProtoFunc::callAsFunction(ExecState *exec, JSObject *thisObj, const List &args)
321 {
322 KJS_CHECK_THIS(KJS::DOMSelection, thisObj);
323
324 DOMSelection *self = static_cast<DOMSelection *>(thisObj);
325 if (!self->attached()) {
326 return jsUndefined();
327 }
328
329 DOM::Selection sel = self->currentSelection();
330
331 DOMExceptionTranslator exception(exec);
332 switch (id) {
333 case DOMSelection::Collapsed: {
334 DOM::NodeImpl *node = toNode(args[0]);
335 int offset = args[1]->toInt32(exec);
336 if (node && node->document() == self->m_document) {
337 self->m_document->part()->setCaret(DOM::Selection(DOM::Position(node, offset)));
338 } else {
339 setDOMException(exec, DOMException::WRONG_DOCUMENT_ERR);
340 }
341 break;
342 }
343
344 case DOMSelection::CollapseToStart:
345 if (sel.notEmpty()) {
346 sel.moveTo(sel.start());
347 self->m_document->part()->setCaret(sel);
348 } else {
349 setDOMException(exec, DOMException::INVALID_STATE_ERR);
350 }
351 break;
352
353 case DOMSelection::CollapseToEnd:
354 if (sel.notEmpty()) {
355 sel.moveTo(sel.end());
356 self->m_document->part()->setCaret(sel);
357 } else {
358 setDOMException(exec, DOMException::INVALID_STATE_ERR);
359 }
360 break;
361
362 case DOMSelection::SelectAllChildren: {
363 DOM::NodeImpl *node = toNode(args[0]);
364 if (node && node->document() == self->m_document) {
365 DOM::RangeImpl *range = new DOM::RangeImpl(self->m_document);
366 range->selectNodeContents(node, exception);
367 self->m_document->part()->setCaret(DOM::Selection(DOM::Range(range)));
368 } else {
369 setDOMException(exec, DOMException::WRONG_DOCUMENT_ERR);
370 }
371 break;
372 }
373
374 case DOMSelection::DeleteFromDocument: {
375 self->m_document->part()->setCaret(DOM::Selection());
376 DOM::Range r = sel.toRange();
377 DOM::RangeImpl *ri = r.handle();
378 if (ri) {
379 ri->deleteContents(exception);
380 }
381 break;
382 }
383
384 case DOMSelection::GetRangeAt: {
385 int i = args[0]->toInt32(exec);
386 if (sel.isEmpty() || i != 0) {
387 setDOMException(exec, DOMException::INDEX_SIZE_ERR);
388 } else {
389 DOM::Range r = sel.toRange();
390 return getDOMRange(exec, r.handle());
391 }
392 break;
393 }
394
395 case DOMSelection::AddRange: {
396 // We only support a single range, so we merge the two.
397 // This does violate HTML5, though, as it's actually supposed to report the
398 // overlap twice. Perhaps this shouldn't be live?
399 DOM::RangeImpl *range = toRange(args[0]);
400
401 if (!range) {
402 return jsUndefined();
403 }
404 if (range->ownerDocument() != self->m_document) {
405 setDOMException(exec, DOMException::WRONG_DOCUMENT_ERR);
406 return jsUndefined();
407 }
408
409 if (sel.isEmpty()) {
410 self->m_document->part()->setCaret(DOM::Selection(range));
411 return jsUndefined();
412 }
413
414 DOM::Range ourRange = sel.toRange();
415 DOM::RangeImpl *ourRangeImpl = ourRange.handle();
416
417 bool startExisting = ourRangeImpl->compareBoundaryPoints(DOM::Range::START_TO_START, range, exception) == -1;
418 bool endExisting = ourRangeImpl->compareBoundaryPoints(DOM::Range::END_TO_END, range, exception) == -1;
419
420 DOM::RangeImpl *rangeForStart = startExisting ? ourRangeImpl : range;
421 DOM::RangeImpl *rangeForEnd = endExisting ? ourRangeImpl : range;
422 DOM::Position start = DOM::Position(rangeForStart->startContainer(exception), rangeForStart->startOffset(exception));
423 DOM::Position end = DOM::Position(rangeForEnd->endContainer(exception), rangeForEnd->endOffset(exception));
424
425 self->m_document->part()->setCaret(DOM::Selection(start, end));
426 break;
427 }
428
429 case DOMSelection::RemoveRange: {
430 // This actually take a /Range/. How brittle.
431 if (sel.isEmpty()) {
432 return jsUndefined();
433 }
434
435 DOM::RangeImpl *range = toRange(args[0]);
436 DOM::Range ourRange = sel.toRange();
437 DOM::RangeImpl *ourRangeImpl = ourRange.handle();
438 if (range && range->startContainer(exception) == ourRangeImpl->startContainer(exception)
439 && range->startOffset(exception) == ourRangeImpl->startOffset(exception)
440 && range->endContainer(exception) == ourRangeImpl->endContainer(exception)
441 && range->endOffset(exception) == ourRangeImpl->endOffset(exception)) {
442 self->m_document->part()->setCaret(DOM::Selection());
443 }
444 break;
445 }
446
447 case DOMSelection::RemoveAllRanges:
448 self->m_document->part()->setCaret(DOM::Selection());
449 break;
450
451 case DOMSelection::ToString:
452 if (sel.isEmpty() || sel.isCollapsed()) {
453 return jsString(UString());
454 } else {
455 DOM::Range r = sel.toRange();
456 DOM::RangeImpl *ri = r.handle();
457 if (ri) {
458 return jsString(ri->toString(exception));
459 }
460 }
461 break;
462 }
463 return jsUndefined();
464 }
465
currentSelection() const466 DOM::Selection DOMSelection::currentSelection() const
467 {
468 if (m_document && m_document->part()) {
469 return m_document->part()->caret();
470 } else {
471 return DOM::Selection();
472 }
473 }
474
attached() const475 bool DOMSelection::attached() const
476 {
477 return m_document && m_document->part();
478 }
479
480 }
481