Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
123 changes: 99 additions & 24 deletions api/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,75 @@ let waitForSingleElements = [];

var allWaitInstances = {};
let totalRunners = 0;
let scratchToolsSelectorScanTimer = null;
let scratchToolsSelectorScanInProgress = false;
let scratchToolsSelectorScanRerunRequested = false;

function safelyRunWaitForElementsCallback(waitInstance, element) {
try {
let callbackResult = waitInstance.callback(element);
if (callbackResult && typeof callbackResult.then === "function") {
Promise.resolve(callbackResult).catch(function (error) {
ste.console.error(error, "waitForElements");
});
}
} catch (error) {
ste.console.error(error, "waitForElements");
}
}

function processWaitInstanceElements(key, waitInstance) {
if (!waitInstance || waitInstance.removed) {
delete allWaitInstances[key];
return;
}

if (!waitInstance.seenElements) {
waitInstance.seenElements = new WeakSet();
}

try {
document.querySelectorAll(waitInstance.selector).forEach(function (el) {
if (!waitInstance.seenElements.has(el)) {
waitInstance.seenElements.add(el);
waitInstance.elements.push(el);
safelyRunWaitForElementsCallback(waitInstance, el);
}
});
} catch (error) {
ste.console.error(error, "waitForElements");
}
}

function processSingleWaitElements() {
waitForSingleElements = waitForSingleElements.filter(function (promise) {
if (promise.resolved) {
return false;
}

let element = document.querySelector(promise.selector);
if (element) {
promise.resolved = true;
promise.resolve(element);
return false;
}

return true;
});
}

function scheduleScratchToolsSelectorScan() {
if (scratchToolsSelectorScanTimer !== null) {
return;
}

// Batch frequent DOM mutations into one scan per frame-ish interval.
scratchToolsSelectorScanTimer = setTimeout(function () {
scratchToolsSelectorScanTimer = null;
returnScratchToolsSelectorsMutationObserverCallbacks();
}, 16);
}

ScratchTools.waitForElements = function (selector, callback) {
totalRunners += 1;
var thisRunner = "wait-" + (totalRunners - 1).toString();
Expand All @@ -150,12 +219,16 @@ ScratchTools.waitForElements = function (selector, callback) {
selector,
callback,
elements: [],
seenElements: new WeakSet(),
};
returnScratchToolsSelectorsMutationObserverCallbacks();
processWaitInstanceElements(thisRunner, allWaitInstances[thisRunner]);
processSingleWaitElements();
return {
id: thisRunner,
remove: function () {
allWaitInstances[thisRunner].removed = true;
if (allWaitInstances[thisRunner]) {
allWaitInstances[thisRunner].removed = true;
}
},
};
};
Expand All @@ -169,21 +242,23 @@ ScratchTools.waitForElements("head > *", function (el) {

ScratchTools.waitForElement = async function (selector) {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
let existingElement = document.querySelector(selector);
if (existingElement) {
resolve(existingElement);
} else {
waitForSingleElements.push({
selector: selector,
resolved: false,
resolve,
});
scheduleScratchToolsSelectorScan();
}
});
};

function enableScratchToolsSelectorsMutationObserver() {
var ScratchToolsSelectorsMutationObserver = new MutationObserver(
returnScratchToolsSelectorsMutationObserverCallbacks
scheduleScratchToolsSelectorScan
);
ScratchToolsSelectorsMutationObserver.observe(
document.querySelector("html"),
Expand All @@ -194,26 +269,26 @@ function enableScratchToolsSelectorsMutationObserver() {
enableScratchToolsSelectorsMutationObserver();

function returnScratchToolsSelectorsMutationObserverCallbacks() {
updateCSSFiles()
Object.keys(allWaitInstances).forEach(function (key) {
var waitInstance = allWaitInstances[key];
if (!waitInstance.removed) {
document.querySelectorAll(waitInstance.selector).forEach(function (el) {
if (!waitInstance.elements?.includes(el)) {
allWaitInstances[key].elements.push(el);
waitInstance.callback(el);
}
});
}
});
waitForSingleElements
.filter((promise) => !promise.resolved)
.forEach(function (promise) {
if (document.querySelector(promise.selector)) {
promise.resolved = true;
promise.resolve(document.querySelector(promise.selector));
}
if (scratchToolsSelectorScanInProgress) {
scratchToolsSelectorScanRerunRequested = true;
return;
}

scratchToolsSelectorScanInProgress = true;
try {
updateCSSFiles();
Object.keys(allWaitInstances).forEach(function (key) {
processWaitInstanceElements(key, allWaitInstances[key]);
});
processSingleWaitElements();
} finally {
scratchToolsSelectorScanInProgress = false;
}

if (scratchToolsSelectorScanRerunRequested) {
scratchToolsSelectorScanRerunRequested = false;
scheduleScratchToolsSelectorScan();
}
}

ScratchTools.createModal = function (titleText, description, buttons) {
Expand Down
205 changes: 133 additions & 72 deletions api/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,88 +25,149 @@ function className(name) {
return "ste-" + name.toLowerCase().replaceAll(" ", "-")
}

ScratchTools.modules.forEach(async function (script) {
var feature = await import(ScratchTools.dir + "/api/feature/index.js");
var shouldBeRun = true;
if (script.runOn) {
shouldBeRun = !!new URL(window.location.href).pathname.match(script.runOn);
async function ensureFeatureRuntimeData(featureId) {
if (!featureId || !Array.isArray(ScratchTools?.Features?.data)) {
return;
}

let existing = ScratchTools.Features.data.find(function (el) {
return el.id === featureId;
});

let needsRuntimeData =
!existing ||
!Array.isArray(existing.resources) ||
!Array.isArray(existing.options);

if (!needsRuntimeData) {
return;
}

let featureData = null;
try {
let response = await fetch(`${ScratchTools.dir}/features/${featureId}/data.json`);
if (response.ok) {
featureData = await response.json();
}
} catch (error) {}

if (!featureData) {
if (!existing) {
ScratchTools.Features.data.push({
id: featureId,
resources: [],
options: [],
localesData: {},
});
}
return;
}
if (script.pageType) {
var pageType = document.querySelector("#app") ? 3 : 2;
shouldBeRun = pageType === script.pageType;

let merged = Object.assign({}, existing || {}, featureData);
merged.id = featureId;
merged.resources = Array.isArray(merged.resources) ? merged.resources : [];
merged.options = Array.isArray(merged.options) ? merged.options : [];
merged.localesData = merged.localesData || existing?.localesData || {};

if (existing) {
Object.assign(existing, merged);
} else {
ScratchTools.Features.data.push(merged);
}
if (shouldBeRun) {
if (!alreadyInjected.includes(script.file)) {
alreadyInjected.push(script.file);
var fun = await import(script.file);
if (fun.default) {
var featureGenerated = feature.default(script.feature)
allFeatures.push(featureGenerated)
fun.default({
feature: featureGenerated,
scratchClass,
className,
console: {
log: function (content) {
ste.console.log(content, script.feature.id);
},
warn: function (content) {
ste.console.warn(content, script.feature.id);
},
error: function (content) {
ste.console.error(content, script.feature.id);
},
},
});
}

function runFeatureEntry(fun, script, featureGenerated) {
let featureLabel = script?.feature?.id || script?.file || "module-loader";
try {
let response = fun.default({
feature: featureGenerated,
scratchClass,
className,
console: {
log: function (content) {
ste.console.log(content, featureLabel);
},
warn: function (content) {
ste.console.warn(content, featureLabel);
},
error: function (content) {
ste.console.error(content, featureLabel);
},
},
});

if (response && typeof response.then === "function") {
Promise.resolve(response).catch(function (error) {
ste.console.error(error, featureLabel);
});
}
} catch (error) {
ste.console.error(error, featureLabel);
}
}

ScratchTools.modules.forEach(async function (script) {
try {
await ensureFeatureRuntimeData(script?.feature?.id);
var feature = await import(ScratchTools.dir + "/api/feature/index.js");
var shouldBeRun = true;
if (script.runOn) {
shouldBeRun = !!new URL(window.location.href).pathname.match(script.runOn);
}
if (script.pageType) {
var pageType = document.querySelector("#app") ? 3 : 2;
shouldBeRun = pageType === script.pageType;
}
if (shouldBeRun) {
if (!alreadyInjected.includes(script.file)) {
alreadyInjected.push(script.file);
var fun = await import(script.file);
if (fun.default) {
var featureGenerated = feature.default(script.feature)
allFeatures.push(featureGenerated)
runFeatureEntry(fun, script, featureGenerated);
}
}
}
} catch (error) {
ste.console.error(error, script?.feature?.id || script?.file || "module-loader");
}
});

ScratchTools.injectModule = async function (script) {
var feature = await import(ScratchTools.dir + "/api/feature/index.js");
var shouldBeRun = true;
if (script.runOn) {
shouldBeRun = !!new URL(window.location.href).pathname.match(script.runOn);
}
if (script.pageType) {
var pageType = document.querySelector("#app") ? 3 : 2;
shouldBeRun = pageType === script.pageType;
}
if (shouldBeRun) {
if (!alreadyInjected.includes(script.file)) {
alreadyInjected.push(script.file);
var fun = await import(script.file);
if (fun.default) {
var featureGenerated = feature.default(script.feature)
allFeatures.push(featureGenerated)
fun.default({
feature: featureGenerated,
scratchClass,
className,
console: {
log: function (content) {
ste.console.log(content, script.feature.id);
},
warn: function (content) {
ste.console.warn(content, script.feature.id);
},
error: function (content) {
ste.console.error(content, script.feature.id);
},
},
});
try {
await ensureFeatureRuntimeData(script?.feature?.id);
var feature = await import(ScratchTools.dir + "/api/feature/index.js");
var shouldBeRun = true;
if (script.runOn) {
shouldBeRun = !!new URL(window.location.href).pathname.match(script.runOn);
}
if (script.pageType) {
var pageType = document.querySelector("#app") ? 3 : 2;
shouldBeRun = pageType === script.pageType;
}
if (shouldBeRun) {
if (!alreadyInjected.includes(script.file)) {
alreadyInjected.push(script.file);
var fun = await import(script.file);
if (fun.default) {
var featureGenerated = feature.default(script.feature)
allFeatures.push(featureGenerated)
runFeatureEntry(fun, script, featureGenerated);
}
} else {
allFeatures.filter((el) => el.self.id === script.feature.id).forEach(function(el) {
el.self.enabled = true
})
ScratchTools.managedElements.filter((el) => el.feature === script.feature.id).forEach(function(el) {
if (!el.element) return;
el.element.style.display = el?.previousDisplay || null
})
allEnableFunctions[script.feature.id]?.();
}
} else {
allFeatures.filter((el) => el.self.id === script.feature.id).forEach(function(el) {
el.self.enabled = true
})
ScratchTools.managedElements.filter((el) => el.feature === script.feature.id).forEach(function(el) {
if (!el.element) return;
el.element.style.display = el?.previousDisplay || null
})
allEnableFunctions[script.feature.id]?.();
}
} catch (error) {
ste.console.error(error, script?.feature?.id || script?.file || "module-loader");
}
};

Expand Down
Loading