Skip to content

Move some JS code from fomantic.js to standalone files (#27994) #28001

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 12, 2023
Merged
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
99 changes: 6 additions & 93 deletions web_src/js/modules/fomantic.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import $ from 'jquery';
import {initAriaCheckboxPatch} from './aria/checkbox.js';
import {initAriaDropdownPatch} from './aria/dropdown.js';
import {initAriaModalPatch} from './aria/modal.js';
import {initFomanticApiPatch} from './fomantic/api.js';
import {initAriaCheckboxPatch} from './fomantic/checkbox.js';
import {initAriaDropdownPatch} from './fomantic/dropdown.js';
import {initAriaModalPatch} from './fomantic/modal.js';
import {initFomanticTransition} from './fomantic/transition.js';
import {svg} from '../svg.js';

export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)');
Expand All @@ -22,100 +24,11 @@ export function initGiteaFomantic() {
return escape(text, preserveHTML) + svg('octicon-x', 16, `${className.delete} icon`);
};

const transitionNopBehaviors = new Set([
'clear queue', 'stop', 'stop all', 'destroy',
'force repaint', 'repaint', 'reset',
'looping', 'remove looping', 'disable', 'enable',
'set duration', 'save conditions', 'restore conditions',
]);
// stand-in for removed transition module
$.fn.transition = function (arg0, arg1, arg2) {
if (arg0 === 'is supported') return true;
if (arg0 === 'is animating') return false;
if (arg0 === 'is inward') return false;
if (arg0 === 'is outward') return false;

let argObj;
if (typeof arg0 === 'string') {
// many behaviors are no-op now. https://fomantic-ui.com/modules/transition.html#/usage
if (transitionNopBehaviors.has(arg0)) return this;
// now, the arg0 is an animation name, the syntax: (animation, duration, complete)
argObj = {animation: arg0, ...(arg1 && {duration: arg1}), ...(arg2 && {onComplete: arg2})};
} else if (typeof arg0 === 'object') {
argObj = arg0;
} else {
throw new Error(`invalid argument: ${arg0}`);
}

const isAnimationIn = argObj.animation?.startsWith('show') || argObj.animation?.endsWith(' in');
const isAnimationOut = argObj.animation?.startsWith('hide') || argObj.animation?.endsWith(' out');
this.each((_, el) => {
let toShow = isAnimationIn;
if (!isAnimationIn && !isAnimationOut) {
// If the animation is not in/out, then it must be a toggle animation.
// Fomantic uses computed styles to check "visibility", but to avoid unnecessary arguments, here it only checks the class.
toShow = this.hasClass('hidden'); // maybe it could also check "!this.hasClass('visible')", leave it to the future until there is a real problem.
}
argObj.onStart?.call(el);
if (toShow) {
el.classList.remove('hidden');
el.classList.add('visible', 'transition');
if (argObj.displayType) el.style.setProperty('display', argObj.displayType, 'important');
argObj.onShow?.call(el);
} else {
el.classList.add('hidden');
el.classList.remove('visible'); // don't remove the transition class because the Fomantic animation style is `.hidden.transition`.
el.style.removeProperty('display');
argObj.onHidden?.call(el);
}
argObj.onComplete?.call(el);
});
return this;
};

initFomanticTransition();
initFomanticApiPatch();

// Use the patches to improve accessibility, these patches are designed to be as independent as possible, make it easy to modify or remove in the future.
initAriaCheckboxPatch();
initAriaDropdownPatch();
initAriaModalPatch();
}

function initFomanticApiPatch() {
//
// Fomantic API module has some very buggy behaviors:
//
// If encodeParameters=true, it calls `urlEncodedValue` to encode the parameter.
// However, `urlEncodedValue` just tries to "guess" whether the parameter is already encoded, by decoding the parameter and encoding it again.
//
// There are 2 problems:
// 1. It may guess wrong, and skip encoding a parameter which looks like encoded.
// 2. If the parameter can't be decoded, `decodeURIComponent` will throw an error, and the whole request will fail.
//
// This patch only fixes the second error behavior at the moment.
//
const patchKey = '_giteaFomanticApiPatch';
const oldApi = $.api;
$.api = $.fn.api = function(...args) {
const apiCall = oldApi.bind(this);
const ret = oldApi.apply(this, args);

if (typeof args[0] !== 'string') {
const internalGet = apiCall('internal', 'get');
if (!internalGet.urlEncodedValue[patchKey]) {
const oldUrlEncodedValue = internalGet.urlEncodedValue;
internalGet.urlEncodedValue = function (value) {
try {
return oldUrlEncodedValue(value);
} catch {
// if Fomantic API module's `urlEncodedValue` throws an error, we encode it by ourselves.
return encodeURIComponent(value);
}
};
internalGet.urlEncodedValue[patchKey] = true;
}
}
return ret;
};
$.api.settings = oldApi.settings;
}
40 changes: 40 additions & 0 deletions web_src/js/modules/fomantic/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import $ from 'jquery';

export function initFomanticApiPatch() {
//
// Fomantic API module has some very buggy behaviors:
//
// If encodeParameters=true, it calls `urlEncodedValue` to encode the parameter.
// However, `urlEncodedValue` just tries to "guess" whether the parameter is already encoded, by decoding the parameter and encoding it again.
//
// There are 2 problems:
// 1. It may guess wrong, and skip encoding a parameter which looks like encoded.
// 2. If the parameter can't be decoded, `decodeURIComponent` will throw an error, and the whole request will fail.
//
// This patch only fixes the second error behavior at the moment.
//
const patchKey = '_giteaFomanticApiPatch';
const oldApi = $.api;
$.api = $.fn.api = function(...args) {
const apiCall = oldApi.bind(this);
const ret = oldApi.apply(this, args);

if (typeof args[0] !== 'string') {
const internalGet = apiCall('internal', 'get');
if (!internalGet.urlEncodedValue[patchKey]) {
const oldUrlEncodedValue = internalGet.urlEncodedValue;
internalGet.urlEncodedValue = function (value) {
try {
return oldUrlEncodedValue(value);
} catch {
// if Fomantic API module's `urlEncodedValue` throws an error, we encode it by ourselves.
return encodeURIComponent(value);
}
};
internalGet.urlEncodedValue[patchKey] = true;
}
}
return ret;
};
$.api.settings = oldApi.settings;
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
54 changes: 54 additions & 0 deletions web_src/js/modules/fomantic/transition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import $ from 'jquery';

export function initFomanticTransition() {
const transitionNopBehaviors = new Set([
'clear queue', 'stop', 'stop all', 'destroy',
'force repaint', 'repaint', 'reset',
'looping', 'remove looping', 'disable', 'enable',
'set duration', 'save conditions', 'restore conditions',
]);
// stand-in for removed transition module
$.fn.transition = function (arg0, arg1, arg2) {
if (arg0 === 'is supported') return true;
if (arg0 === 'is animating') return false;
if (arg0 === 'is inward') return false;
if (arg0 === 'is outward') return false;

let argObj;
if (typeof arg0 === 'string') {
// many behaviors are no-op now. https://fomantic-ui.com/modules/transition.html#/usage
if (transitionNopBehaviors.has(arg0)) return this;
// now, the arg0 is an animation name, the syntax: (animation, duration, complete)
argObj = {animation: arg0, ...(arg1 && {duration: arg1}), ...(arg2 && {onComplete: arg2})};
} else if (typeof arg0 === 'object') {
argObj = arg0;
} else {
throw new Error(`invalid argument: ${arg0}`);
}

const isAnimationIn = argObj.animation?.startsWith('show') || argObj.animation?.endsWith(' in');
const isAnimationOut = argObj.animation?.startsWith('hide') || argObj.animation?.endsWith(' out');
this.each((_, el) => {
let toShow = isAnimationIn;
if (!isAnimationIn && !isAnimationOut) {
// If the animation is not in/out, then it must be a toggle animation.
// Fomantic uses computed styles to check "visibility", but to avoid unnecessary arguments, here it only checks the class.
toShow = this.hasClass('hidden'); // maybe it could also check "!this.hasClass('visible')", leave it to the future until there is a real problem.
}
argObj.onStart?.call(el);
if (toShow) {
el.classList.remove('hidden');
el.classList.add('visible', 'transition');
if (argObj.displayType) el.style.setProperty('display', argObj.displayType, 'important');
argObj.onShow?.call(el);
} else {
el.classList.add('hidden');
el.classList.remove('visible'); // don't remove the transition class because the Fomantic animation style is `.hidden.transition`.
el.style.removeProperty('display');
argObj.onHidden?.call(el);
}
argObj.onComplete?.call(el);
});
return this;
};
}