Skip to content

Commit

Permalink
Merge pull request #672 from openkraken/feat/support_querySelector
Browse files Browse the repository at this point in the history
Feat/support querySelector and querySelectorAll
  • Loading branch information
answershuto committed Sep 23, 2021
2 parents 8cf5e0a + 8ecd064 commit 28a5c77
Show file tree
Hide file tree
Showing 7 changed files with 838 additions and 5 deletions.
38 changes: 36 additions & 2 deletions bridge/bindings/jsc/DOM/document.cc
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,45 @@ JSValueRef JSDocument::getElementsByTagName(JSContextRef ctx, JSObjectRef functi
std::transform(tagName.begin(), tagName.end(), tagName.begin(), ::toupper);

std::vector<ElementInstance *> elements;

traverseNode(document->documentElement, [tagName, &elements](NodeInstance *node) {
if (node->nodeType == NodeType::ELEMENT_NODE) {
auto element = reinterpret_cast<ElementInstance *>(node);
if (element->tagName() == tagName) {
if (element->tagName() == tagName || tagName == "*") {
elements.emplace_back(element);
}
}

return false;
});

JSValueRef elementArguments[elements.size()];

for (int i = 0; i < elements.size(); i++) {
elementArguments[i] = elements[i]->object;
}

return JSObjectMakeArray(ctx, elements.size(), elementArguments, exception);
}

JSValueRef JSDocument::getElementsByClassName(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,
size_t argumentCount, const JSValueRef *arguments, JSValueRef *exception) {
if (argumentCount < 1) {
throwJSError(ctx,
"Uncaught TypeError: Failed to execute 'getElementsByClassName' on 'Document': 1 argument required, "
"but only 0 present.",
exception);
return nullptr;
}

auto document = reinterpret_cast<DocumentInstance *>(JSObjectGetPrivate(thisObject));
JSStringRef classNameStringRef = JSValueToStringCopy(ctx, arguments[0], exception);
std::string className = JSStringToStdString(classNameStringRef);
std::vector<ElementInstance *> elements;

traverseNode(document->documentElement, [ctx, exception, className, &elements](NodeInstance *node) {
if (node->nodeType == NodeType::ELEMENT_NODE) {
auto element = reinterpret_cast<ElementInstance *>(node);
if(element->classNames().containsAll(className)) {
elements.emplace_back(element);
}
}
Expand Down
19 changes: 19 additions & 0 deletions bridge/bindings/jsc/DOM/element.cc
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ JSValueRef ElementInstance::getProperty(std::string &name, JSValueRef *exception
JSElement::ElementProperty &property = propertyMap[name];

switch (property) {
case JSElement::ElementProperty::className: {
std::string str = "class";
JSValueRef classRef = (*m_attributes)->getProperty(str, exception);
return classRef;
break;
}
case JSElement::ElementProperty::nodeName:
case JSElement::ElementProperty::tagName: {
return JSValueMakeString(_hostClass->ctx, JSStringCreateWithUTF8CString(tagName().c_str()));
Expand Down Expand Up @@ -322,6 +328,11 @@ bool ElementInstance::setProperty(std::string &name, JSValueRef value, JSValueRe
auto &property = propertyMap[name];

switch (property) {
case JSElement::ElementProperty::className: {
std::string str = "class";
(*m_attributes)->setProperty(str, value, exception);
break;
}
case JSElement::ElementProperty::style:
case JSElement::ElementProperty::attributes:
return false;
Expand Down Expand Up @@ -756,6 +767,14 @@ void ElementInstance::setAttributes(JSHostObjectHolder<JSElementAttributes> &att
m_attributes = JSHostObjectHolder<JSElementAttributes>(attributes);
}

SpaceSplitString ElementInstance::classNames() {
std::string str = "className";
JSValueRef classValueRef = getProperty(str, nullptr);
JSStringRef classStringRef = JSValueToStringCopy(ctx, classValueRef, nullptr);
std::string strClass = JSStringToStdString(classStringRef);
return SpaceSplitString(strClass);
}

void ElementInstance::internalSetTextContent(JSStringRef content, JSValueRef *exception) {
auto node = firstChild();
while (node != nullptr) {
Expand Down
73 changes: 70 additions & 3 deletions bridge/include/kraken_bridge_jsc.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,68 @@ class MouseEventInstance;
struct NativePopStateEvent;
class PopStateEventInstance;

class SpaceSplitString {
public:
SpaceSplitString() = default;
SpaceSplitString(std::string& string) {
set(string);
}

void set(std::string &string) {
size_t pos = 0;
std::string token;
std::string s = string;
while ((pos = s.find(m_delimiter)) != std::string::npos) {
token = s.substr(0, pos);
m_szData.push_back(token);
s.erase(0, pos + m_delimiter.length());
}
m_szData.push_back(s);
}

bool contains(std::string& string) {
for (std::string s : m_szData) {
if (s == string) {
return true;
}
}

return false;
}

bool containsAll(std::string string) {
std::vector<std::string> szData;
size_t pos = 0;
std::string token;
std::string s = string;

while ((pos = s.find(m_delimiter)) != std::string::npos) {
token = s.substr(0, pos);
szData.push_back(token);
s.erase(0, pos + m_delimiter.length());
}
szData.push_back(s);

bool flag = true;
for(std::string str : szData) {
bool isContains = false;
for(std::string data : m_szData) {
if (data == str) {
isContains = true;
break;
}
}
flag &= isContains;
}

return flag;
}

private:
std::string m_delimiter = " ";
std::vector<std::string> m_szData;
};

class JSContext {
public:
static std::vector<JSStaticFunction> globalFunctions;
Expand Down Expand Up @@ -661,6 +723,9 @@ class JSDocument : public JSNode {
static JSValueRef getElementsByTagName(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,
size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception);

static JSValueRef getElementsByClassName(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,
size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception);

private:
protected:
JSDocument() = delete;
Expand All @@ -674,6 +739,7 @@ class JSDocument : public JSNode {
JSFunctionHolder m_createComment{context, prototypeObject, this, "createComment", createComment};
JSFunctionHolder m_getElementById{context, prototypeObject, this, "getElementById", getElementById};
JSFunctionHolder m_getElementsByTagName{context, prototypeObject, this, "getElementsByTagName", getElementsByTagName};
JSFunctionHolder m_getElementsByClassName{context, prototypeObject, this, "getElementsByClassName", getElementsByClassName};
};

class DocumentCookie {
Expand All @@ -697,7 +763,7 @@ struct NativeDocument {
class DocumentInstance : public NodeInstance {
public:
DEFINE_OBJECT_PROPERTY(Document, 4, nodeName, all, cookie, documentElement);
DEFINE_PROTOTYPE_OBJECT_PROPERTY(Document, 6, createElement, createTextNode, createComment, getElementById,
DEFINE_PROTOTYPE_OBJECT_PROPERTY(Document, 7, createElement, createTextNode, createComment, getElementById, getElementsByClassName,
getElementsByTagName, createEvent);

static DocumentInstance *instance(JSContext *context);
Expand Down Expand Up @@ -813,9 +879,9 @@ using ElementCreator = ElementInstance *(*)(JSContext *context);

class KRAKEN_EXPORT JSElement : public JSNode {
public:
DEFINE_OBJECT_PROPERTY(Element, 17, style, attributes, nodeName, tagName, offsetLeft, offsetTop, offsetWidth,
DEFINE_OBJECT_PROPERTY(Element, 18, style, attributes, nodeName, tagName, offsetLeft, offsetTop, offsetWidth,
offsetHeight, clientWidth, clientHeight, clientTop, clientLeft, scrollTop, scrollLeft,
scrollHeight, scrollWidth, children);
scrollHeight, scrollWidth, children, className);

DEFINE_PROTOTYPE_OBJECT_PROPERTY(Element, 10, getBoundingClientRect, getAttribute, setAttribute, hasAttribute,
removeAttribute, toBlob, click, scroll, scrollBy, scrollTo);
Expand Down Expand Up @@ -890,6 +956,7 @@ class KRAKEN_EXPORT ElementInstance : public NodeInstance {
JSHostClassHolder& getStyle();
void setStyle(JSHostClassHolder& style);
void setAttributes(JSHostObjectHolder<JSElementAttributes>& attributes);
SpaceSplitString classNames();

NativeElement *nativeElement{nullptr};

Expand Down
1 change: 1 addition & 0 deletions bridge/polyfill/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'es6-promise/dist/es6-promise.auto';
import './dom';
import './query-selector';
import { console } from './console';
import { WebSocket } from './websocket';
import { fetch, Request, Response, Headers } from './fetch';
Expand Down
175 changes: 175 additions & 0 deletions bridge/polyfill/src/query-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
function fetchSelector(str: string, regex: RegExp) {
return {
selectors: str.match(regex) || [],
ruleStr: str.replace(regex, ' ')
};
}

function getElementsBySelector(selector: string): Array<Element | null | HTMLElement> {
let context = document;
let temp, tempElements: Array<Element> = [], elements: Array<Element> = [];
selector = selector.trim();

// If selector starts with *, find all elements.
if (selector.charAt(0) === '*') {
let temps: HTMLCollectionOf<Element> = context.getElementsByTagName('*');
tempElements = Array.from(temps);
}

// Classes. e.g. .row.
let classes: Array<string> = [];
selector = selector.split(' ').map(item => {
if (item && item.charAt(0) === '.') {
temp = fetchSelector(selector, /\.[\w-_]+/g);
classes = classes.concat(temp.selectors)
return temp.ruleStr;
}
return item;
}).join(' ');

// Ids. e.g. #mail-title.
temp = fetchSelector(selector, /#[\w-_]+/g);
let id = temp.selectors ? temp.selectors[0] : null;
selector = temp.ruleStr;

// Attributes. e.g. [rel=external].
temp = fetchSelector(selector, /\[.+?\]/g);
let attributes = temp.selectors;
selector = temp.ruleStr;

// Elements. e.g. header, div.
temp = fetchSelector(selector, /\w+/g);
let els = temp.selectors;
selector = temp.ruleStr;

// Get by id.
// Id is supposed to be unique.
// More need to attach other selectors.
if (id) {
id = id.substring(1);
return [document.getElementById(id) || null];
}

// Get by Elements.
if (els.length !== 0) {
let temps: HTMLCollectionOf<Element> = context.getElementsByTagName(els[0]);
tempElements = tempElements.concat(Array.from(temps));
}

// Get by class name.
for (let i = 0, l = classes.length; i !== l; ++i) {
let className = classes[i].substring(1);
let temps: HTMLCollectionOf<Element> = context.getElementsByClassName(className);
let arrTemps: Array<Element> = Array.from(temps);
if (tempElements.length === 0) {
// If no temp elements yet, push into tempElements directly.
tempElements = tempElements.concat(arrTemps);
}
else {
// Otherwise, find intersection.
let prevs: Array<Element> = [];
prevs = prevs.concat(tempElements);
tempElements = [];

for (let index = 0; index < arrTemps.length; index++) {
let t = arrTemps[index];
if (prevs.indexOf(t) !== -1) {
tempElements = tempElements.concat([t]);
}
}
}
}

// Get by attributes.
if (attributes.length !== 0) {
let attrs = {};
for (let i = 0; i < attributes.length; i++) {
let attribute = attributes[i];
attribute = attribute.substring(1, attribute.length - 1);
let parts: Array<string> = (attribute.split('=')).map(item => item.trim());
if (parts[1]) {
parts[1] = parts[1].substring(1, parts[1].length - 1);
}
attrs[parts[0]] = parts[1];
}
let prevs: Array<Element> = [];
prevs = prevs.concat(tempElements);
tempElements = [];

for (let i = 0, l = prevs.length; i !== l; ++i) {
let t = prevs[i];
let shouldAdd = true;
for (let key in attrs) {
let lastChar = key.charAt(key.length - 1);
if (/[\^\*\$]$/.test(key)) {
key = key.substring(0, key.length - 1);
}
let tempAttr = t.getAttribute(key) || '';
// Case: [href*=/en].
if (lastChar === '*' && tempAttr.indexOf(attrs[key + lastChar]) === -1) {
shouldAdd = false;
break;
}
// Case: [href^=/en].
else if (lastChar === '^' && tempAttr.indexOf(attrs[key + lastChar]) !== 0) {
shouldAdd = false;
break;
}
// Case: [href$=/en].
else if (lastChar === '$' &&
(tempAttr.lastIndexOf(attrs[key + lastChar]) === -1
? false
: tempAttr.lastIndexOf(attrs[key + lastChar]))
!==
tempAttr.length - attrs[key + lastChar].length) {
shouldAdd = false;
break;
}
// Case: [href=/en].
else if (/[\$\*\^]/.test(lastChar) === false && tempAttr !== attrs[key]) {
shouldAdd = false;
break;
}

}

if (shouldAdd) {
tempElements = tempElements.concat([t]);
}
}
}

elements = elements.concat(tempElements);
return elements;
}

document.querySelectorAll = function <E extends Element = Element>(selector: string): NodeListOf<E> {
if (typeof selector !== 'string') {
throw new TypeError('document.querySelectorAll: Invalid selector type. ' +
'Expect: string. Found: ' + typeof selector + '.');
}
let elements: Array<E> = [];

// Split `selector` into rules by `,`.
let rules: Array<string> = selector.split(',').map(item => item.trim());

// Iterate through each rule.
// For the sake of performance, use for-loop here rather than forEach.
for (let i = 0, l = rules.length; i !== l; ++i) {
let rule = rules[i];

// TODO: support ' ' and '>'.
elements = elements.concat(getElementsBySelector.call(this, rule));
}

return (elements as any) as NodeListOf<E>;
};

document.querySelector = function (selector: string): Element | null {
if (typeof selector !== 'string') {
throw new TypeError('document.querySelector: Invalid selector type. ' +
'Expect: string. Found: ' + typeof selector + '.');
}
let elements = this.querySelectorAll(selector);
return elements.length > 0 ? elements[0] : null;
}

0 comments on commit 28a5c77

Please sign in to comment.