Using the Debut theme in Shopify, I have a product with variants that are not compatible. At present they show as unavailable but I want to hide them totally so that only valid options show with each other. So as example if I have Red, Blue, Green shoes and have sizes 9,10,11. But can only get Red shoes in Size 10. I don't want to see options for 9 or 11 ever.
Online someone pointed to theme.js and the code below, but I'm not sure what to change.
Thanks
$(this.singleOptionSelector, this.$container).on(
'change',
this._onSelectChange.bind(this)
);
}
I've just spend most of the day on this, and have come up with the following, which seems to work nicely on the site I'm developing.
The answer I came up with involves editing the assets/theme.js file. At present, the code below disables the select options which are not relevant by checking them against the available variant combinations, but you could easily adapt the below to hide them and then show them instead with CSS.
assets/theme.js
The _hideUnavailableOptions method below needs to be added to the Variants.prototype object.
You then need to call the method from two different places, see below.
_hideUnavailableOptions: function() {
const option1 = document.getElementById("SingleOptionSelector-0");
const option2 = document.getElementById("SingleOptionSelector-1");
if (option2) {
const secondOptions = option2.options;
const variants = this.product.variants;
let possibles = [];
variants.forEach((variant) => {
if (variant.options.includes(option1.value)) {
possibles.push(variant.options)
}
})
for (let option of secondOptions) {
const value = option.value;
let flag = false;
possibles.forEach((possible) => {
if (possible.includes(value)) {
flag = true;
}
})
if (flag === false) {
option.removeAttribute('selected');
option.setAttribute('disabled', 'disabled');
} else {
option.removeAttribute('disabled');
}
} option2.querySelector(':not([disabled="disabled"])').setAttribute('selected', 'selected');
}
},
Call the method as follows:
function Variants(options) {
//stuff before this, then...
this.enableHistoryState = options.enableHistoryState;
this._hideUnavailableOptions(); //N.B. this MUST be before the next line
this.currentVariant = this._getVariantFromOptions();
}
...and again, call the method from Variants.prototype._onSelectChange() - make sure it's the first line in there...
_onSelectChange: function() {
let hideUnavailable = this._hideUnavailableOptions(); //N.B. this MUST be before the next line
var variant = this._getVariantFromOptions();
//lots more stuff follows...
}
Related
I'm trying to add auto-completions for mermaid diagrams to my editor:
const mermaids = Object.entries({
"mermaid graph": `graph LR\n x --> y`,
}).map(([name, autocompletion]) => ({
caption: name,
meta: name,
value: "``mermaid\n" + autocompletion + "\n```"
}));
aceeditor.setOptions({
enableBasicAutocompletion: [{
getCompletions: (editor, session, pos, prefix, callback) => {
callback(null, [
...mermaids
])
}
}],
enableSnippets: false,
enableLiveAutocompletion: true
});
In the resulting editor, if the user types "graph" or "mermaid" and hits enter to auto-complete, it works as expected. (With the exception of less-than desirable cursor position after the completion.) If the user types "```" and hits enter, the autocompletion occurs after the originally typed "```". E.g.,
``````mermaid
graph LR
x --> y
\``` <-- just escaped here for SO's sake
Is there an efficient way to correct this? If not, what event can I use to determine when an auto-completion has actually occurred and search for duplicate markers?
Is there a better way to do this in general?
I think the broader issue here is that your completion item more closely resembles a snippet. Using snippets would also give you better control over where the cursor goes after insertion.
Answering your second question, Autocomplete.insertMatch is the function responsible for inserting the chosen completion item. You could hook it, or perhaps use the mysteriously undocumented .completer field on the completion items? It's been there for 9 years now, perhaps it is not an accident.
const FakeCompleter = {
insertMatch: (editor, data) => {
// stolen from default insertMatch, needed to erase text that triggered our completion:
if (editor.completer.completions.filterText) {
var ranges = editor.selection.getAllRanges()
for (var i = 0, range; range = ranges[i]; i++) {
range.start.column -= editor.completer.completions.filterText.length
editor.session.remove(range)
}
}
// if there are ` symbols immediately before cursor, omit those from completion:
let text = (data.value || data)
const lead = editor.selection.lead
const prefix = editor.session.getLine(lead.row).substring(0, lead.column)
const mt = /.*?(`{1,3})$/.exec(prefix)
if (mt && text.startsWith(mt[1])) text = text.substring(mt[1].length)
// and finally call the regular insertion:
editor.execCommand("insertstring", text)
}
}
const mermaids = Object.entries({
"mermaid graph": `graph LR\n x --> y`,
}).map(([name, autocompletion]) => ({
caption: name,
meta: name,
value: "``mermaid\n" + autocompletion + "\n```",
completer: FakeCompleter
}));
I'm having some trouble migrating one thing from the old addon-knobs to the new controls. Let me explain, maybe it's not such difficult task but I'm blocked at the moment.
I'm using StencilJS to generate Web Components and I have a custom select component that accepts a options prop, this is an array of objects (the options of the select)
So, the story for this component in the previous version of Storybook looks something like this:
export const SelectWithArray = () => {
const selectElement = document.createElement('my-select');
selectElement.name = name;
selectElement.options = object('Options', options);
selectElement.disabled = boolean('Disabled', false);
selectElement.label = text('Label', 'Label');
return selectElement;
};
This works fine, the select component receives the options property correctly as an array of objects.
Now, migrating this to the new Storybook version without addon-knobs, the story is looking like this:
const TemplateWithArray: Story<ISelect> = (args) => {
return `
<my-select
label="${args.label}"
disabled="${args.disabled}"
options="${args.options}"
>
</my-select>
`;
};
export const SelectWithArray: Story<ISelect> = TemplateWithArray.bind({});
SelectWithArray.argTypes = {
options: {
name: 'Options',
control: { type: 'object' },
}
}
SelectWithArray.args = {
options: [
{ text: 'Option 1', value: 1 },
]
}
And with this new method, the component is not able to receive the property as expected.
I believe the problem is that now, the arguments is being set directly on the HTML (which would only be accepting strings) and before it was being set on the JS part, so you could set attributes other than strings.
Is there a way to achieve this? without having to send the arguments as a string.
Thanks a lot!!
One way I've discovered so far is to bind the object after the canvas has loaded via the .play function;
codeFullArgs.play = async () => {
const component = document.getElementsByTagName('your-components-tag')[0];
component.jobData = FullArgs.args.jobData;
}
so I have to use cy.contains to find the element I want, but all I can find online is how to use if() with cy.find or cy.get if there a way to do this with contains?
Example code:
if(cy.contains('div.name', 'Test 1').length > 0) {
//Type in name
cy.get('input.newName').click().type('Test 1');
cy.wait(2000);
//Click Add Name
cy.get('div.createNewName> a').click();
cy.wait(2000);
}
What I am trying to do there is:
if(Name doesnt exist){
Create it
}
I'm not sure if I have explained myself too well, if any more clarifications are needed feel free to ask
You can also do like this:
cy.get('body').then(($body) => {
if ($body.find('div.name:contains("Test 1")').length > 0) {
//Element Found
} else {
//Element not found
}
})
The general pattern for this would be as follows:
const element = Cypress.$('div.name:contains(Test 1)')
if (element.length > 0) {
...
Make sure the DOM is stable when you run this code, there is no retry built in as there is with cy.contains()
If the code inside if() is creating the name, then maybe the logic would be
const element = Cypress.$('div.name:contains(Test 1)')
if (element.length === 0) {
// not found so create it
...
You can also do it like this
cy.get('div.name').then($div => {
const found = $div.find(':contains("Test 1")')
if (found.length === 0) {
// create...
}
})
Is there a way to hide some Functions/Operators in the Row Filter of APEX 19.1's Interactive Report? Some end-users get confused with as many functions/operators that they do not used.
Thanks for any considerations.
While APEX doesn't support this out of the box, it is doable with JavaScript. Every time the filter dialog is displayed, content is brought from the server to the client and injected into the DOM. You just need to modify the content before the user can see it. One way to achieve this is by using the MutationObserver interface: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
Here are some steps you could follow to do it (tested in APEX 19.2):
Go to the Interactive Report and set the Static ID to my-irr.
Go to the page level attributes and add the following code to the Function and Global Variable Declaration field:
function removeIRFilterOperators() {
var irRegId = 'my-irr';
var filterOperatorsToRemove = ['!=', 'ABS'];
var observer;
function detectFilterDialog(mutationsList) {
for (var mIdx = 0; mIdx < mutationsList.length; mIdx++) {
if (mutationsList[mIdx].addedNodes.length &&
mutationsList[mIdx].addedNodes[0].classList &&
mutationsList[mIdx].addedNodes[0].classList.contains('a-IRR-dialog--filter')) {
removeOperators();
}
}
}
function removeOperators() {
var anchors = document.querySelectorAll('#' + irRegId + '_row_filter_operators a');
for (var aIdx = 0; aIdx < anchors.length; aIdx++) {
if (filterOperatorsToRemove.includes(anchors[aIdx].textContent)) {
anchors[aIdx].parentElement.parentElement.removeChild(anchors[aIdx].parentElement);
}
}
}
observer = new MutationObserver(detectFilterDialog);
observer.observe(
document,
{
attributes: false,
childList: true,
subtree: true
}
);
}
removeIRFilterOperators();
The MutationObverver uses the detectFilterDialog function to detect when the filter dialog is added to the DOM. When that happens, the removeOperators function removes the specified options from the operator's list. All you need to do is update the filterOperatorsToRemove array to include the list of operators you want to remove.
If you're talking about the "Actions" menu, then yes - go to IR's attributes and enable/disable any option you want:
Is there a way in JXA to get multiple properties from multiple objects with a single call?
For example, I want to get name and enabled property from menu items which can be done for each individual property as follows:
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.name()
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.enabled()
but is it possible to get them with a single function call? Something like:
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.select('name', 'enabled')
I know, that I can iterate through the menuBarItems and collect properties from .properties() method, but this approach is too slow, that's why I'm looking for other options.
UPDATE
I'm looking for better performance, not for nicer syntax, i.e. I want properties to be retrieved in a single call to System Events.
I'd probably do it like this:
sys = Application('com.apple.systemevents');
FinderProc = sys.processes['Finder'];
FinderMenuBarItems = FinderProc.menuBars[0].menuBarItems();
Array.from(FinderMenuBarItems,x=>[x.name(),x.enabled()]);
By first converting the object to an array, this allows one to map each element and retrieve the desired properties for all in one go. The code is split over several lines for ease of reading.
EDIT: added on 2019-07-27
Following on from your comment regarding Objective-C implementation, I had a bit of time today to write a JSObjc script. It does the same thing as the vanilla JXA version above, and, yes, it clearly makes multiple function calls, which is necessary. But it's performing these functions at a lower level than System Events (which isn't involved at all here), so hopefully you'll find it more performant.
ObjC.import('ApplicationServices');
ObjC.import('CoreFoundation');
ObjC.import('Foundation');
ObjC.import('AppKit');
var err = {
'-25211':'APIDisabled',
'-25206':'ActionUnsupported',
'-25205':'AttributeUnsupported',
'-25204':'CannotComplete',
'-25200':'Failure',
'-25201':'IllegalArgument',
'-25202':'InvalidUIElement',
'-25203':'InvalidUIElementObserver',
'-25212':'NoValue',
'-25214':'NotEnoughPrecision',
'-25208':'NotImplemented',
'-25209':'NotificationAlreadyRegistered',
'-25210':'NotificationNotRegistered',
'-25207':'NotificationUnsupported',
'-25213':'ParameterizedAttributeUnsupported',
'0':'Success'
};
var unwrap = ObjC.deepUnwrap.bind(ObjC);
var bind = ObjC.bindFunction.bind(ObjC);
bind('CFMakeCollectable', [ 'id', [ 'void *' ] ]);
Ref.prototype.nsObject = function() {
return unwrap($.CFMakeCollectable(this[0]));
}
function getAttrValue(AXUIElement, AXAttrName) {
var e;
var _AXAttrValue = Ref();
e = $.AXUIElementCopyAttributeValue(AXUIElement,
AXAttrName,
_AXAttrValue);
if (err[e]!='Success') return err[e];
return _AXAttrValue.nsObject();
}
function getAttrValues(AXUIElement, AXAttrNames){
var e;
var _AXAttrValues = Ref();
e = $.AXUIElementCopyMultipleAttributeValues(AXUIElement,
AXAttrNames,
0,
_AXAttrValues);
if (err[e]!='Success') return err[e];
return _AXAttrValues.nsObject();
}
function getAttrNames(AXUIElement) {
var e;
var _AXAttrNames = Ref();
e = $.AXUIElementCopyAttributeNames(AXUIElement, _AXAttrNames);
if (err[e]!='Success') return err[e];
return _AXAttrNames.nsObject();
}
(() => {
const pid_1 = $.NSWorkspace.sharedWorkspace
.frontmostApplication
.processIdentifier;
const appElement = $.AXUIElementCreateApplication(pid_1);
const menuBar = getAttrValue(appElement,"AXMenuBar");
const menuBarItems = getAttrValue(menuBar, "AXChildren");
return menuBarItems.map(x => {
return getAttrValues(x, ["AXTitle", "AXEnabled"]);
});
})();