FIX: Multiple placeholders in a single post (#40)
The changes in 948634fe31
meant that only the most-recently-changed placeholder is actually applied.
This commit refactors things so that we store all placeholder values in JS, and then apply them all in a single pass over the DOM. As well as fixing the bug, this should be a significant perf improvement for posts with lots of placeholders
Also introduces some simple system specs.
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
parent
cbea5f6471
commit
569b566d38
|
@ -21,8 +21,8 @@ function buildInput(key, placeholder) {
|
||||||
input.setAttribute("placeholder", placeholder.description);
|
input.setAttribute("placeholder", placeholder.description);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (placeholder.default) {
|
if (placeholder.value) {
|
||||||
input.value = placeholder.default;
|
input.value = placeholder.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
|
@ -57,13 +57,49 @@ function buildSelect(key, placeholder) {
|
||||||
placeholder.defaults.forEach((value) =>
|
placeholder.defaults.forEach((value) =>
|
||||||
addSelectOption(select, {
|
addSelectOption(select, {
|
||||||
value,
|
value,
|
||||||
selected: placeholder.default === value,
|
selected: placeholder.value === value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return select;
|
return select;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function performReplacements(cooked, placeholders) {
|
||||||
|
cooked.querySelectorAll(VALID_TAGS).forEach((elem) => {
|
||||||
|
const textNodeWalker = document.createTreeWalker(
|
||||||
|
elem,
|
||||||
|
NodeFilter.SHOW_TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
while (textNodeWalker.nextNode()) {
|
||||||
|
const node = textNodeWalker.currentNode;
|
||||||
|
|
||||||
|
if (!originalContentMap.has(node)) {
|
||||||
|
// Haven't seen this node before. Get the text, and store it for future transformations
|
||||||
|
originalContentMap.set(node, node.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalText = originalContentMap.get(node);
|
||||||
|
let text = originalText;
|
||||||
|
|
||||||
|
for (const [key, { delimiter, value }] of Object.entries(placeholders)) {
|
||||||
|
const placeholderWithDelimiter = `${delimiter}${key}${delimiter}`;
|
||||||
|
|
||||||
|
let substitution = value;
|
||||||
|
if (!substitution?.length || substitution === "none") {
|
||||||
|
substitution = placeholderWithDelimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.replaceAll(placeholderWithDelimiter, substitution);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.data !== text) {
|
||||||
|
node.data = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "discourse-placeholder-theme-component",
|
name: "discourse-placeholder-theme-component",
|
||||||
|
|
||||||
|
@ -120,69 +156,16 @@ export default {
|
||||||
const key = inputEvent.target.dataset.key;
|
const key = inputEvent.target.dataset.key;
|
||||||
const placeholder = placeholders[inputEvent.target.dataset.key];
|
const placeholder = placeholders[inputEvent.target.dataset.key];
|
||||||
const placeholderIdentifier = `${postIdentifier}${key}`;
|
const placeholderIdentifier = `${postIdentifier}${key}`;
|
||||||
const placeholderWithDelimiter = `${placeholder.delimiter}${key}${placeholder.delimiter}`;
|
|
||||||
|
|
||||||
if (value) {
|
if (value && value !== placeholder.default) {
|
||||||
if (value !== placeholder.default) {
|
placeholder.value = value;
|
||||||
this.setValue(placeholderIdentifier, value);
|
this.setValue(placeholderIdentifier, value);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
placeholder.value = placeholder.default;
|
||||||
this.removeValue(placeholderIdentifier);
|
this.removeValue(placeholderIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
let newValue;
|
performReplacements(cooked, placeholders);
|
||||||
if (value && value.length && value !== "none") {
|
|
||||||
newValue = value;
|
|
||||||
} else {
|
|
||||||
newValue = placeholderWithDelimiter;
|
|
||||||
}
|
|
||||||
|
|
||||||
cooked.querySelectorAll(VALID_TAGS).forEach((elem) => {
|
|
||||||
const textNodeWalker = document.createTreeWalker(
|
|
||||||
elem,
|
|
||||||
NodeFilter.SHOW_TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
while (textNodeWalker.nextNode()) {
|
|
||||||
const node = textNodeWalker.currentNode;
|
|
||||||
let text;
|
|
||||||
|
|
||||||
if (originalContentMap.has(node)) {
|
|
||||||
// The content of this node has already been transformed. Use the value
|
|
||||||
// we saved as the source of truth
|
|
||||||
text = originalContentMap.get(node);
|
|
||||||
} else {
|
|
||||||
// Haven't seen this node before. Get the text, and store it for future
|
|
||||||
// transformations
|
|
||||||
text = node.data;
|
|
||||||
originalContentMap.set(node, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
node.data = text.replaceAll(placeholderWithDelimiter, newValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const _fillPlaceholders = () => {
|
|
||||||
if (Object.keys(placeholders).length > 0) {
|
|
||||||
// trigger fake event to setup initial state
|
|
||||||
Object.keys(placeholders).forEach((placeholderKey) => {
|
|
||||||
const placeholder = placeholders[placeholderKey];
|
|
||||||
const placeholderIdentifier = `${postIdentifier}${placeholderKey}`;
|
|
||||||
const value =
|
|
||||||
this.getValue(placeholderIdentifier) || placeholder.default;
|
|
||||||
|
|
||||||
processChange({
|
|
||||||
target: {
|
|
||||||
value,
|
|
||||||
dataset: {
|
|
||||||
key: placeholderKey,
|
|
||||||
delimiter: placeholder.delimiter,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const placeholderNodes = cooked.querySelectorAll(
|
const placeholderNodes = cooked.querySelectorAll(
|
||||||
|
@ -203,10 +186,11 @@ export default {
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
placeholders[dataKey] = {
|
placeholders[dataKey] = {
|
||||||
default: valueFromStore || elem.dataset.default,
|
default: elem.dataset.default,
|
||||||
defaults: defaultValues,
|
defaults: defaultValues,
|
||||||
delimiter: elem.dataset.delimiter || DELIMITER,
|
delimiter: elem.dataset.delimiter || DELIMITER,
|
||||||
description: elem.dataset.description,
|
description: elem.dataset.description,
|
||||||
|
value: valueFromStore || elem.dataset.default,
|
||||||
};
|
};
|
||||||
|
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
|
@ -245,7 +229,7 @@ export default {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
later(_fillPlaceholders, 500);
|
later(performReplacements, cooked, placeholders, 500);
|
||||||
},
|
},
|
||||||
{ onlyStream: true, id: "discourse-placeholder-theme-component" }
|
{ onlyStream: true, id: "discourse-placeholder-theme-component" }
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
RSpec.describe "Placeholder", system: true do
|
||||||
|
let(:theme) { Fabricate(:theme) }
|
||||||
|
let!(:component) { upload_theme_component(parent_theme_id: theme.id) }
|
||||||
|
|
||||||
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
theme.set_default!
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when using default attribute" do
|
||||||
|
fab!(:post) do
|
||||||
|
Fabricate(
|
||||||
|
:post,
|
||||||
|
raw: "[wrap=placeholder key=\"TEST\" default=\"foo\"][/wrap]\n\nBEFORE =TEST= AFTER",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "replaces keys on load" do
|
||||||
|
topic_page.visit_topic(post.topic)
|
||||||
|
|
||||||
|
expect(page).to have_content("BEFORE foo AFTER")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when using multiple placeholders" do
|
||||||
|
fab!(:post) do
|
||||||
|
Fabricate(
|
||||||
|
:post,
|
||||||
|
raw:
|
||||||
|
"[wrap=placeholder key=\"TEST1\"][/wrap]\n\n[wrap=placeholder key=\"TEST2\"][/wrap]\n\nBEFORE =TEST1= =TEST2= AFTER",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "replaces each of them" do
|
||||||
|
topic_page.visit_topic(post.topic)
|
||||||
|
|
||||||
|
page.find('.discourse-placeholder-value[data-key="TEST1"]').fill_in(with: "foo")
|
||||||
|
|
||||||
|
expect(page).to have_content("BEFORE foo =TEST2= AFTER")
|
||||||
|
|
||||||
|
page.find('.discourse-placeholder-value[data-key="TEST2"]').fill_in(with: "bar")
|
||||||
|
|
||||||
|
expect(page).to have_content("BEFORE foo bar AFTER")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue