CKEditor 5 - writer.setAttribute('title', ...) on img element doesn't work - ckeditor

I am creating a plugin for CKEditor 5, and I can't figure out how to set the title attribute on an <img> element using writer.setAttribute('title', ...).
I have tried stuff like schema.extend but to no avail.
The thing is, code works flawlessly when operating on the alt attribute.
Am I missing something?
My plugin code:
const ButtonView = require('#ckeditor/ckeditor5-ui/src/button/buttonview').default;
const imageIcon = require('#ckeditor/ckeditor5-core/theme/icons/low-vision.svg').default;
export default class ImageTextTitle extends Plugin {
init() {
const editor = this.editor;
editor.ui.componentFactory.add('imageTextTitle', locale => {
const view = new ButtonView(locale);
view.set({
label: 'Insert image title',
icon: imageIcon,
tooltip: true
});
view.on('execute', () => {
const newTitle = prompt('New image title');
const selection = editor.model.document.selection;
const imageElement = selection.getSelectedElement();
if (newTitle !== null) {
editor.model.change(writer => {
writer.setAttribute('title', newTitle, imageElement);
});
}
});
return view;
});
}
}

Related

Is it possible to BehaviorSubject reset my useState value?

Problem: Whenever I click 't' key my theme changed as I expected. But for some reason this cause isModalVisible state reset. This cause one problem - When I have my modal open and click t - the modal disapear. I don't know why. Maybe BehaviorSubject cause the problem? Additionally, if I create another useState for test, which initial value is string 'AAA', and on open modal I set this state to string 'BBB'. Then I click 't' to change theme this test useState back to beginning value 'AAA'
My code looks like this:
const Component1 = () => {
const [mode, setMode] = useRecoilState(selectedModeState);
const { switcher, status } = useThemeSwitcher();
const toggleMode = (newTheme: MODE_TYPE) => {
theme$.next({ theme: newTheme });
localStorage.setItem('theme', newTheme);
};
useEffect(() => {
const mode_sub = theme$.subscribe(({ theme }) => {
switcher({ theme: theme });
setMode(theme);
});
return () => {
mode_sub.unsubscribe();
};
}, []);
return <Component2 toggleMode={toggleMode} currentMode={mode} />
}
const Component2 = ({toggleMode, currentMode}) => {
const [mode, setMode] = useState(currentMode);
const [savedTheme, setSavedTheme] = useRecoilState(selectedThemeState);
const getTheme = mode === MODE_TYPE.LIGHT ? MODE_TYPE.DARK : MODE_TYPE.LIGHT;
const themeListener = (event: KeyboardEvent) => {
switch (event.key) {
case 't':
setMode(getTheme);
toggleMode(getTheme);
break;
}
};
useEffect(() => {
document.addEventListener('keydown', themeListener);
document.body.setAttribute('data-theme', savedTheme);
localStorage.setItem('color', savedTheme);
return () => {
document.removeEventListener('keydown', themeListener);
};
}, [savedTheme]);
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
return (
<Modal isModalVisible={isModalVisible} setIsModalVisible={setIsModalVisible} />
)
}
UTILS:
export const selectedModeState = atom<MODE_TYPE>({
key: 'selectedThemeState',
default: (localStorage.getItem('theme') as MODE_TYPE) || MODE_TYPE.LIGHT,
});
export const selectedThemeState = atom<string>({
key: 'selectedColorState',
default: (localStorage.getItem('color') as string) || 'blue',
});
export const theme$ = new BehaviorSubject({
theme: (localStorage.getItem('theme') as THEME_TYPE) || THEME_TYPE.LIGHT,
});
I would like the theme change not to set visibleModal to false which causes the modal to close

Shopify 2.0 Dawn - How to hide unavailable variants?

PROBLEM
Unavailable variant combinations are different from sold-out because customers don't understand it's the selects which make certain combinations 'not possible'.
Shopify's way of handling this is to display 'Unavailable' in the buy button. But customers think this means sold-out when in reality, they've chosen the wrong combination of variants...
The previous JS workarounds to remove unavailable or 'not possible' variants don't work in Shopify 2.0's new default/flagship theme, Dawn because the JS is different.
As far as I can tell, Dawn's variant JS was recently moved from /asstes/variants.js to line 497 in /assets/global.js.
SKILL
My CSS is decent but my JS is lame, I'm a designer sorry.
QUESTIONS
Based on user interaction with the first variant, how do you hide unavailable variants (not sold-out) in Shopify 2.0 Dawn?
How do you make one variant option set a checkbox instead of a radio button or radio?
What's the best way to add custom text as the first option in selects? e.g. 'Choose a size...' or 'Choose a color...' etc. Is it best to hard-code or use JS for this as well?
RESOURCES/EXAMPLES
Here's a pull request which grabs sold-out from the new Dawn JS but I don't understand how to adapt it for 'Unavailable' sorry (which is a different exception from sold-out): https://github.com/Shopify/dawn/pull/105
Here's an example of how to hide unavailable variants in the older Debut theme which doesn’t seem to work in the newer Dawn JS: https://www.youtube.com/watch?v=vspWDu_POYA
Here's a link to the JS gist referenced in that video: https://gist.github.com/jonathanmoore/c0e0e503aa732bf1c05b7a7be4230c61
And finally, here's the new code from Dawn at line 497 in /assets/global.js
class VariantSelects extends HTMLElement {
constructor() {
super();
this.addEventListener('change', this.onVariantChange);
}
onVariantChange() {
this.updateOptions();
this.updateMasterId();
this.toggleAddButton(true, '', false);
this.updatePickupAvailability();
if (!this.currentVariant) {
this.toggleAddButton(true, '', true);
this.setUnavailable();
} else {
this.updateMedia();
this.updateURL();
this.updateVariantInput();
this.renderProductInfo();
}
}
updateOptions() {
this.options = Array.from(this.querySelectorAll('select'), (select) => select.value);
}
updateMasterId() {
this.currentVariant = this.getVariantData().find((variant) => {
return !variant.options.map((option, index) => {
return this.options[index] === option;
}).includes(false);
});
}
updateMedia() {
if (!this.currentVariant || !this.currentVariant?.featured_media) return;
const newMedia = document.querySelector(
`[data-media-id="${this.dataset.section}-${this.currentVariant.featured_media.id}"]`
);
if (!newMedia) return;
const parent = newMedia.parentElement;
parent.prepend(newMedia);
window.setTimeout(() => { parent.scroll(0, 0) });
}
updateURL() {
if (!this.currentVariant) return;
window.history.replaceState({ }, '', `${this.dataset.url}?variant=${this.currentVariant.id}`);
}
updateVariantInput() {
const productForms = document.querySelectorAll(`#product-form-${this.dataset.section}, #product-form-installment`);
productForms.forEach((productForm) => {
const input = productForm.querySelector('input[name="id"]');
input.value = this.currentVariant.id;
input.dispatchEvent(new Event('change', { bubbles: true }));
});
}
updatePickupAvailability() {
const pickUpAvailability = document.querySelector('pickup-availability');
if (!pickUpAvailability) return;
if (this.currentVariant?.available) {
pickUpAvailability.fetchAvailability(this.currentVariant.id);
} else {
pickUpAvailability.removeAttribute('available');
pickUpAvailability.innerHTML = '';
}
}
renderProductInfo() {
fetch(`${this.dataset.url}?variant=${this.currentVariant.id}&section_id=${this.dataset.section}`)
.then((response) => response.text())
.then((responseText) => {
const id = `price-${this.dataset.section}`;
const html = new DOMParser().parseFromString(responseText, 'text/html')
const destination = document.getElementById(id);
const source = html.getElementById(id);
if (source && destination) destination.innerHTML = source.innerHTML;
document.getElementById(`price-${this.dataset.section}`)?.classList.remove('visibility-hidden');
this.toggleAddButton(!this.currentVariant.available, window.variantStrings.soldOut);
});
}
toggleAddButton(disable = true, text, modifyClass = true) {
const addButton = document.getElementById(`product-form-${this.dataset.section}`)?.querySelector('[name="add"]');
if (!addButton) return;
if (disable) {
addButton.setAttribute('disabled', true);
if (text) addButton.textContent = text;
} else {
addButton.removeAttribute('disabled');
addButton.textContent = window.variantStrings.addToCart;
}
if (!modifyClass) return;
}
setUnavailable() {
const addButton = document.getElementById(`product-form-${this.dataset.section}`)?.querySelector('[name="add"]');
if (!addButton) return;
addButton.textContent = window.variantStrings.unavailable;
document.getElementById(`price-${this.dataset.section}`)?.classList.add('visibility-hidden');
}
getVariantData() {
this.variantData = this.variantData || JSON.parse(this.querySelector('[type="application/json"]').textContent);
return this.variantData;
}
}
customElements.define('variant-selects', VariantSelects);
class VariantRadios extends VariantSelects {
constructor() {
super();
}
updateOptions() {
const fieldsets = Array.from(this.querySelectorAll('fieldset'));
this.options = fieldsets.map((fieldset) => {
return Array.from(fieldset.querySelectorAll('input')).find((radio) => radio.checked).value;
});
}
}
customElements.define('variant-radios', VariantRadios);
Any help or pointers in the right direction would be much appreciated. Cheers

Nativescript imagepicker not working in iOS :: not picking up image path?

I am using Nativescript with Angular and have a page where I photograph a receipt or add from gallery and add a couple of text inputs and send to server.
The Add from gallery is working fine in Android but not in iOS.
Here is the template code:
<Image *ngIf="imageSrc" [src]="imageSrc" [width]="previewSize" [height]="previewSize" stretch="aspectFit"></Image>
<Button text="Pick from Gallery" (tap)="onSelectGalleryTap()" class="btn-outline btn-photo"> </Button>
and the component:
public onSelectGalleryTap() {
let context = imagepicker.create({
mode: "single"
});
let that = this;
context
.authorize()
.then(() => {
that.imageAssets = [];
that.imageSrc = null;
return context.present();
})
.then((selection) => {
alert("Selection done: " + JSON.stringify(selection));
that.imageSrc = selection.length > 0 ? selection[0] : null;
// convert ImageAsset to ImageSource
fromAsset(that.imageSrc).then(res => {
var myImageSource = res;
var base64 = myImageSource.toBase64String("jpeg", 20);
this.expense.receipt_data=base64;
})
that.cameraImage=null;
that.imageAssets = selection;
that.galleryProvided=true;
// set the images to be loaded from the assets with optimal sizes (optimize memory usage)
selection.forEach(function (element) {
element.options.width = that.previewSize;
element.options.height = that.previewSize;
});
}).catch(function (e) {
console.log(e);
});
}
I have posted below the Android and iOS screenshots of the line:
alert("Selection done: " + JSON.stringify(selection));
In Android there is a path to the location of the image in the file system but in iOS there are just empty curly brackets where I'd expect to see the path and then when submitted the message back is "Unable to save image" although the image preview is displaying on the page in Image.
Here are the screenshots:
Android:
iOS:
Any ideas why it is failing in iOS?
Thanks
==========
UPDATE
I am now saving the image to a temporary location and it is still not working in iOS. It works in Android.
Here is my code now.
import { ImageAsset } from 'tns-core-modules/image-asset';
import { ImageSource, fromAsset, fromFile } from 'tns-core-modules/image-source';
import * as fileSystem from "tns-core-modules/file-system";
...
...
public onSelectGalleryTap() {
alert("in onSelectGalleryTap");
var milliseconds=(new Date).getTime();
let context = imagepicker.create({
mode: "single"
});
let that = this;
context
.authorize()
.then(() => {
that.imageAssets = [];
that.previewSrc = null;
that.imageSrc = null;
return context.present();
})
.then((selection) => {
that.imageSrc = selection.length > 0 ? selection[0] : null;
// convert ImageAsset to ImageSource
fromAsset(that.imageSrc)
.then(res => {
var myImageSource = res;
let folder=fileSystem.knownFolders.documents();
var path=fileSystem.path.join(folder.path, milliseconds+".jpg");
var saved=myImageSource.saveToFile(path, "jpg");
that.previewSrc=path;
const imageFromLocalFile: ImageSource = <ImageSource> fromFile(path);
var base64 = imageFromLocalFile.toBase64String("jpeg", 20);
this.expense.receipt_data=base64;
})
that.cameraImage=null;
that.imageAssets = selection;
that.galleryProvided=true;
// set the images to be loaded from the assets with optimal sizes (optimize memory usage)
selection.forEach(function (element) {
element.options.width = that.previewSize;
element.options.height = that.previewSize;
});
}).catch(function (e) {
console.log(e);
});
}
Any ideas? Thanks.
It is an already communicated issue, several of us subscribed for, check here issue #321
for updates.

How to make a context menu in a custom ckeditor5 widget?

I made a inline widget similar a placeholder (ckeditor4), but now I want to render a dropdown when the widget is selected to show options values to replace the placeholder. I trying use BalloonPanelView but no success until now, someone have a idea about how to make it?
this.editor.editing.view.document.on('click', (evt, data) => {
evt.stop();
const element = data.target;
if (element && element.hasClass('placeholder')) {
if (!element.getAttribute('data-is-fixed')) {
const balloonPanelView = new BalloonPanelView();
balloonPanelView.render();
['option1', 'option2', 'option3'].forEach((value) => {
const view = new View();
view.set({
label: value,
withText: true
});
balloonPanelView.content.add(view);
});
balloonPanelView.pin({
target: element
});
}
}
});
I found the solution using ContextualBalloon class:
import ContextualBalloon from "#ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon";
// Define ballon
const balloon = editor.plugins.get(ContextualBalloon);
const placeholderOptions = // Here I defined list with buttons '<li><button></li>'
// Finnaly render ballon
balloon.add({
view: placeholderOptions,
singleViewMode: true,
position: {
target: data.domTarget
}
});

tabBarOnPress stopped working react-navigation 3.3.2

After upgrading to react-navigation 3.3.2 the tabBarOnPress no longer fires. Code snippet below. Any ideas?
const Tabs = createBottomTabNavigator(
{
HomeStack,
WallStack,
MemeStack,
},
{
navigationOptions: ({navigation}) => ({
tabBarOnPress: () => {
console.log('tab pressed');
}
})
}
);
export default DrawerNav = createDrawerNavigator({
Tabs: Tabs
},{
drawerBackgroundColor: Colors.grayDark,
contentComponent: Sidebar
})
tabBarOnPress is working, but I'm using it like this. You need to pass it as a property of each stack. (don't forget to call the defaultHandler to keep the default behavior:
const DashboardStack = createStackNavigator(
{
Dashboard,
}
);
DashboardStack.navigationOptions = {
tabBarOnPress({ navigation, defaultHandler }) {
// do something
defaultHandler();
},
};
const ProfileStack = createStackNavigator(
{
Profile,
AccountInfo,
Membership,
Browser,
ConnectedAccounts,
}
);
ProfileStack.navigationOptions = {
tabBarOnPress({ navigation, defaultHandler }) {
// do something
defaultHandler();
},
};
const SpendingStack = createStackNavigator(
{
Budget,
Transactions,
}
);
SpendingStack.navigationOptions = {
tabBarOnPress({ navigation, defaultHandler }) {
// do something
defaultHandler();
},
};
export default createBottomTabNavigator(
{
SpendingStack,
DashboardStack,
ProfileStack,
}
);

Resources