آموزش clone یا نمونه سازی از تگ های html در جاوا اسکریپت

آموزش clone یا نمونه سازی از تگ های html در جاوا اسکریپت

clone از html با جاوا اسکریپت و کتابخانه jquery cloner ، با کمک این کتابخانه می توانیم خیلی راحت از تگ های html نمونه بگیریم .

این پروژه را در 2 سطح انجام می دهیم :

  1. نمونه عادی
  2. نمونه تو در تو



خروجی برنامه


در نمونه تو در تو از تقویم فارسی ، select2 و نمونه در نمونه استفاده می کنیم .

ویژگی که این پروژه دارد این است که در هر تغییر input های فرم از جمله select در hidden داده ها به صورت آرایه ذخیره می شود .

جزئیات dataset ها

  1. data-seperator : زمانی که بخواهیم اعداد را به صورت 3 رقم 3 رقم جدا کنیم از این ویژگی استفاده می کنیم
  2. data-value : این ویژگی مورد شماره 1 مرتبط است یا موارد مشابه در صورتی که بخواهیم مقدار 1,234 را به صورت 1234 ذخیره کنیم به کار مان می آید .
  3. data-optional : در صورتی که بخواهیم بگوییم این فیلد در لیست مان اختیاری است این dataset به کار مان می آید . دلیل آن این است که هر مقدار که در hidden ذخیره باشد و دارای مقدار null باشد فرم ناقص تلقی می شود با این ویژگی به جای null مقدار “” ذخیره می کنیم .
  4. data-empty : این ویژگی فقط برای فیلد هایی که کلاس data-field ذخیره می شود همان hidden که داده ها را به صورت آرایه ذخیره می کند و در صورتی که در آرایه فیلدی را کاربر کامل نکرده باشد این ویژگی ظاهر می شود .


نمونه عادی

<!DOCTYPE html>
<html lang="fa">

<head>
    <meta charset="UTF-8">
    <title>Rapidcode.iR - سورس کد</title>
    <link rel="stylesheet" href="static/css/lib/persian-datepicker-custom.min.css">
    <link rel="stylesheet" href="static/css/lib/persian-datepicker.min.css">
    <link rel="stylesheet" href="static/css/lib/select2.min.css">
    <link rel="stylesheet" href="static/css/main.css">
</head>

<body>
    <div class="container">
        <a id="introduce" href="https://rapidcode.ir" target="_blank">رپید کد • کتابخانه مجازی برنامه نویسان</a>
        <h1>تکرار شونده عادی - clone simple elements</h1>
        <div class="form-group clonable-block-parent specification">
            <label id="specification-label" class="control-label">مشخصات</label>
            <br>
            <div class="custom-control custom-checkbox">
                <input type="checkbox" data-parent="specification" class="custom-control-input activatation" name="specification-activate" id="specification-activate">
                <label class="custom-control-label" for="specification-activate">فعال / غیرفعال</label>
            </div>

            <hr>
            <table>
                <tbody class="clonable-block clone_specification" id="clone_specification" data-parent="specification">
                    <tr class="clonable" data-funcs='["loopThrowIndexLabelCloner"]'>
                        <td>
                            <label for="specification_1" class="clonable-increment-for highlight-orange">مشخصه <span class="clonable-html-number">1</span></label>
                            <select id="specification_1" data-name="term_id_specification" data-parent="clone_specification" class="form-control input-json select2 select2ajax clipboardIT clonable-increment-id" data-taxonomy="specification" data-clipboard-id="specification_name"></select>
                            <button type="button" class="clonable-button-close btn btn-danger">حذف</button>
                            <button type="button" class="clonable-button-add btn btn-info">افزودن</button>
                        </td>

                        <td>
                            <input type="text" data-name="term_val_specification" class="input-json drag-top specification_value form-control clonable-increment-id clonable-increment-name" id="specification_value_1" placeholder="مقدار مشخصه">
                        </td>

                    </tr>
                </tbody>
            </table>
            <input type="hidden" id="specification-terms" data-empty="true" class="data-field" name="specification-terms" value="" data-label="specification-label">
        </div>

    </div>
    <script src="static/js/lib/jquery.min.js"></script>
    <script src="static/js/lib/jquery.cloner.min.js"></script>
    <script src="static/js/lib/persian-date.min.js"></script>
    <script src="static/js/lib/persian-datepicker.min.js"></script>
    <script src="static/js/lib/select2.min.js"></script>
    <script src="static/js/lib/select2fa.min.js"></script>

    <script src="static/js/app.js"></script>
</body>

</html>


نمونه تو در تو

<!DOCTYPE html>
<html lang="fa">

<head>
    <meta charset="UTF-8">
    <title>Rapidcode.iR - سورس کد</title>
    <link rel="stylesheet" href="static/css/lib/persian-datepicker-custom.min.css">
    <link rel="stylesheet" href="static/css/lib/persian-datepicker.min.css">
    <link rel="stylesheet" href="static/css/lib/select2.min.css">
    <link rel="stylesheet" href="static/css/main.css">
</head>

<body>
    <div class="container">
        <a id="introduce" href="https://rapidcode.ir" target="_blank">رپید کد • کتابخانه مجازی برنامه نویسان</a>
       
        <h1>تکرار شونده تودرتو - clone nested elements</h1>
        <div class="form-group clonable-block-parent variation">
            <label id="variation-label" class="control-label">ویژگی ها</label>
            <div class="custom-control custom-checkbox">
                <input type="checkbox" data-parent="variation" class="custom-control-input activatation" name="variation-activate" id="variation-activate">
                <label class="custom-control-label" for="variation-activate">فعال / غیرفعال</label>
            </div>
            <hr>
            <table>
                <tbody class="clonable-block clone_variation" id="clone_variation" data-parent="variation">
                    <tr class="clonable depth-1-row" data-funcs='["loopThrowIndexLabelCloner"]'>
                        <td>
                            <label class="highlight-orange">ویژگی <span data-parent="depth-1-row" class="clonable-html-number">1</span></label><br><br>

                            <table>
                                <tbody id="depth-2-1" class="clonable-block depth-2-1 depth-2" data-parent="variation">
                                    <tr class="clonable depth-2-row" data-funcs='["loopThrowIndexLabelCloner"]'>
                                        <td><label for="variation_1" class="clonable-increment-for">نام ویژگی <span data-parent="depth-2-row" class="clonable-html-number">1</span></label>
                                            <select id="variation_1" data-name="term_id_variation" data-parent="depth-2-1" class="form-control input-json select2 select2ajax clipboardIT clonable-increment-id" data-taxonomy="variation" data-clipboard-id="variation_name"></select>
                                            <button type="button" class="clonable-button-close btn btn-danger">حذف</button>
                                            <button type="button" class="clonable-button-add btn btn-info">افزودن</button>
                                        </td>
                                        <td>
                                            <input id="variation_value_1" data-name="term_val_variation" type="text" class="input-json drag-top form-control clonable-increment-id" placeholder="محتوای ویژگی">
                                        </td>
                                    </tr>
                                </tbody>
                            </table>

                            <table id="depth-2-2" class="depth-2 regular">
                                <tr>
                                    <td><label for="guarantee_1" class="clonable-increment-for">گارانتی</label>
                                        <select id="guarantee_1" data-optional="true" data-name="term_id_guarantee" data-parent="clone_variation" class="form-control input-json select2 select2ajax clipboardIT clonable-increment-id" data-taxonomy="guarantee" data-clipboard-id="variation_guarantee"></select><br><br>
                                    </td>
                                    <td><label for="guarantee_price_1" class="clonable-increment-for">هزینه گارانتی ( تومان )</label><br><br>
                                        <input id="guarantee_price_1" data-optional="true" data-seperator="true" data-name="term_val_guarantee" type="text" class="input-json drag-top form-control clonable-increment-id">
                                    </td>
                                </tr>

                                <tr>
                                    <td><label for="enitity_1" class="clonable-increment-for">موجودی</label>
                                        <input id="enitity_1" data-seperator="true" data-name="variation_entity" type="text" data-parent="clone_variation" class="form-control input-json clonable-increment-id"><br>
                                    </td>
                                </tr>

                                <tr>
                                    <td> <label for="price_1" class="clonable-increment-for">قیمت عادی ( تومان )</label>
                                        <input id="price_1" data-seperator="true" data-name="variation_price" type="text" data-parent="clone_variation" class="form-control input-json clonable-increment-id"><br>
                                    </td>
                                    <td> <label for="price_sale_1" class="clonable-increment-for">قیمت حراج ( تومان )</label>
                                        <input id="price_sale_1" data-optional="true" data-seperator="true" data-name="variation_price_sale" type="text" data-parent="clone_variation" class="form-control input-json clonable-increment-id"><br>
                                    </td>
                                    <td><label for="price_sale_expire_date_1" class="clonable-increment-for">تاریخ انقضاء حراج</label>
                                        <input type="text" data-optional="true" data-name="variation_price_sale_expire" id="price_sale_expire_date_1" data-parent="clone_variation" class="form-control input-json date-picker-shamsi clonable-increment-id clipboardIT" data-clipboard-id="variation_expire_date">
                                    </td>
                                </tr>
                            </table>


                            <button type="button" class="clonable-button-close btn btn-danger">حذف</button>
                            <button type="button" class="clonable-button-add btn btn-info">افزودن</button>



                        </td>


                    </tr>
                </tbody>
            </table>
            <input type="hidden" id="variation-terms" data-empty="true" class="data-field" name="variation-terms" value="" data-label="variation-label">
        </div>

    </div>
    <script src="static/js/lib/jquery.min.js"></script>
    <script src="static/js/lib/jquery.cloner.min.js"></script>
    <script src="static/js/lib/persian-date.min.js"></script>
    <script src="static/js/lib/persian-datepicker.min.js"></script>
    <script src="static/js/lib/select2.min.js"></script>
    <script src="static/js/lib/select2fa.min.js"></script>

    <script src="static/js/app.js"></script>
</body>

</html>


فایل main.css

.container {
    margin: 0 auto;
    width: 80%;
    text-align: center;
    direction: rtl;
}

#introduce {
    display: block;
    width: 100%;
    font-size: 35px;
    font-weight: bold;
    color: white;
    padding-bottom: 5px;
    background-color: #4CAF50;
    text-decoration: none;
    margin-bottom: 15px;
}


/* Page Style */

.select2{
    width: 200px !important;
}


اسکریپت app.js

$(document).ready(function(e){


window.generatePersianDatepickerFeature = function (element, options = {}) {
    const res = element.persianDatepicker(options);
    return res;
}

window.generateSelect2Feature = function (element, options = {}) {
    const res = element.select2(options);
    return res;
}

window.destroySelect2Feature = function (element) {
    element.select2("destroy");
}

window.showSelect2AjaxResult = function (data, li) {

    if (data.loading || !data.id) {
        return data.text;
    }

    var container = $(`<div class="result-select2-ajax">${data.name}</div>`);
    return container;
}

window.showSelection2AjaxResult = function (data) {
    return data.name || "انتخاب کنید";
}

window.onDeleteRow = function (e) {
    const currentElement = $(e.target);
    const clone = currentElement.parents('.clonable').first();
    const cloneBlock = clone.parent('.clonable-block').first();
    const firstClone = cloneBlock.find('.clonable').first();

    
    setTimeout(function (clone) {
        if (clone.length) {
            triggerCloneInput(clone);
        }
    }, 100, firstClone)

}

window.select2Clear = function (e) {
    const targetElement = $(e.target);
    const targetElementVal = targetElement.val();
    if (!targetElementVal) {
        targetElement.empty();
    }
}

window.loopThrowClonableLocal = function (clone, index, callback = null) {
    const parent = clone.parent();
    const cloneClassList = clone.attr('class');
    const selector = generateCssClass(cloneClassList, ['clonable-source', 'clonable-clone']);
    const cloneList = parent.find(selector);

    if (callback) {
        return callback({
            clone: clone,
            index: index,
            cloneList: cloneList
        });
    }
}

window.callListedFunctions = function (args) {
    const funcList = JSON.parse(args.clone.attr('data-funcs'));
    for (const ch of funcList) {
        args.cloneList.each(window[ch]);
    }
}

window.generateCssClass = function (classListStr, except = []) {
    let cssSelector = classListStr;

    for (const ex of except) {
        cssSelector = cssSelector.replace(ex, "");
    }

    cssSelector = cssSelector.replace(/\s{2,}/gi, " ").replace(" ", ".").replace(/\.\s|\.$/gi, "").trim();
    cssSelector = "." + cssSelector;

    return cssSelector;
}

window.loopThrowIndexLabelCloner = function (index, element) {
    element = $(element);
    const theIndex = index + 1;
    let depthChecker = false;
    if (element.attr("class").search("depth-") != -1) {
        depthChecker = true;
    }

    const elements = element.find('.clonable-html-number');

    for (const elementChild of elements) {
        if (depthChecker) {
            if (element.attr("class").search($(elementChild).attr('data-parent')) != -1) {
                $(elementChild).html(theIndex);
            }
            continue;
        }
        $(elementChild).html(theIndex);
    }

}

window.getUniqueIdDOM = function(DOM , id , seperator = "_t_"){
    let theID = id ? id : DOM.attr('id');
    theID = theID.split(seperator);
    theID = theID[0];
    const finallID = theID + seperator + Date.now().toString().slice(-4);
    DOM.attr('id' , finallID);
    return finallID;
}

window.generateUniqueIdDom = function(clone , except = []){
    const theExcept = except.join(",");
    const inputs = clone.find(`.input-json:not(${theExcept})`);
    
    for(let input of inputs){
        input = $(input);
        getUniqueIdDOM(input);
    }
}

window.select2ClonerFixer = function (clone, index = 0) {

    let parentElement = clone.parent();

    const select2Selector = `.select2[data-parent=${parentElement.attr('id')}]`;

    let select2 = clone.find(select2Selector);

    if (!select2.length) return;

    const cloneInDepth = clone.find('.clonable');
    if (cloneInDepth.length) select2ClonerFixer(cloneInDepth.eq(0));

    let select2PreviousElement = select2.prev();
    let select2NextElement = select2.next();

    const select2ID = select2.attr('data-clipboard-id');
    const classList = select2.attr('class');
    const id = select2PreviousElement.attr('for');

    select2.remove();
    select2NextElement.remove();

    const DOM = $(window[select2ID]);
    DOM.attr('class', classList);
    getUniqueIdDOM(DOM , id);

    select2PreviousElement.after(DOM)

    select2 = clone.find(select2Selector);

    generateSelect2Feature(select2, select2AjaxOptions)
}

window.persianDatePickerClonerFixer = function (clone, index) {
    const datePickerDOM = clone.find('input.date-picker-shamsi');

    if (!datePickerDOM.length) return;

    const datePickerDOMPrevious = datePickerDOM.prev();
    const datePickerID = datePickerDOM.attr('id');

    const DOM = $(window[datePickerDOM.attr('data-clipboard-id')]);

    DOM.attr('id', datePickerID);

    datePickerDOM.remove();

    datePickerDOMPrevious.after(DOM);

    generatePersianDatepickerFeature(DOM, shamsiDatePickerOptions);

}

window.cloneActivation = function (e) {
    const currentElement = $(e.target);
    const checked = currentElement.prop('checked');
    const parent = $('.' + currentElement.attr('data-parent'));
    const dataField = parent.find('.data-field');
    if (checked) {
        loopThrowInputsJsonClass('.input-json', parent, emptyInputs);
        loopThrowInputsJsonClass('.input-json', parent, disableInputs);
        dataField.val('');
        dataField.removeAttr("data-empty");
    } else {
        loopThrowInputsJsonClass('.input-json', parent, enableInputs);
        dataField.attr("data-empty", "true");
    }

    parent.toggleClass('cover-disable');


}

window.onInputNumberSeperator = function (e) {
    const thisElement = $(e.target);
    const value = thisElement.val().replace(/,/gi, "").replace(/\D/gi, "");

    if (isNaN(Number(value))) return;

    let seperatedValue = new Intl.NumberFormat('en-US', { style: "decimal" }).format(value);

    if (seperatedValue == 0) seperatedValue = '';

    thisElement.val(seperatedValue);
    thisElement.attr('data-value', value);
}

window.generateDataInputsClonable = function (args) {
    const dataField = args.parent.find('.data-field');
    for (let input of args.inputs) {
        input = $(input);
        const targetEvent = getEventTarget(input);
        if (!targetEvent) continue;

        input.off(targetEvent, window.valueListenerClonable);

        input.on(targetEvent, null, {
            dataField: dataField,
            inputs: args.inputs
        }, window.valueListenerClonable);


    }
}

window.loopThrowClonableGlobal = function (clone, index, callback) {
    const cloneParent = clone.parent();
    const cloneParentParent = $('.' + cloneParent.attr('data-parent'));
    const cloneList = cloneParentParent.find(".clonable");

    if (callback) {
        return callback({
            clone: clone,
            index: index,
            cloneParentParent: cloneParentParent,
            cloneList: cloneList
        })
    }
}

window.loopThrowInputsClonable = function (args) {
    const inputs = args.cloneParentParent.find('.input-json');

    return {
        parent: args.cloneParentParent,
        inputs: inputs
    };
}

window.emptyInputs = function (input) {
    const eventTarget = getEventTarget(input);
    input.val(null).trigger(eventTarget);
}

window.disableInputs = function (input) {
    input.attr('disabled', 'disabled');
}

window.enableInputs = function (input) {
    input.removeAttr('disabled');
}

window.valueListenerClonable = function (e) {

    const dataField = $(e.data.dataField);
    const inputs = $(e.data.inputs);

    let jsonData = [];

    let key = '';
    let val = '';
    const inputListID = [];

    function checkDuplicateInput(list , element){
        if (list.includes(element) || $('#' + element).length == 0) {
            return true;
        }

        return false;
    }

    for (let input of inputs) {
        const theID = $(input).attr('id');
        if(checkDuplicateInput(inputListID , theID)) continue;

        input = $(input);
        const row = loopToGetParent(input, 'data-funcs');
        const jsonData2 = [];
        if (row.length) {

            const rowInputs = row.find('.input-json');
            
            for (let rowInput of rowInputs) {
                const theID = $(rowInput).attr('id');
                if(checkDuplicateInput(inputListID , theID)) continue;
                
                rowInput = $(rowInput);
                inputListID.push(rowInput.attr('id'));
                
                key = rowInput.attr('data-name');

                if (rowInput.attr('data-value') && rowInput.val())
                    val = rowInput.attr('data-value');
                else
                    val = rowInput.val() ? rowInput.val() : null;

                if (rowInput.attr('data-optional') && val === null)
                    val = '';


                const objMap = {}
                objMap[key] = val;
                objMap['id'] = rowInput.attr('id');

                jsonData2.push(objMap);
            }

            jsonData.push(jsonData2)

        }



    }

    jsonData = JSON.stringify(jsonData);
    console.log(jsonData);
    dataField.val(jsonData)
    makeItDataEmpty(dataField)

}

window.loopToGetParent = function (element, dataAttr) {
    let parent = null;
    let counter = 0
    while (true) {
        if (parent && (parent.attr(dataAttr) || counter == 15)) break;

        parent = !parent ? element.parent() : parent.parent();

        counter++;
    }

    if (parent && parent.parent().hasClass('depth-2')) return loopToGetParent(parent, dataAttr)

    return parent;
}

window.makeItDataEmpty = function (element) {

    const val = JSON.parse(element.val());

    propertyList.DOM = element;
    loopThrowObjects(val, indexerObjects);

    propertyListReset([
        "DOM",
        "loopBreak"
    ]);

}

window.loopThrowObjects = function (objectList, callback) {

    if (typeof objectList === "object" && objectList != null) {
        const keys = Object.keys(objectList);
        for (const key of keys) {
            if (propertyList.loopBreak) return;
            const mElement = objectList[key]
            callback(mElement);
        }
    }

}

window.indexerObjects = function (element) {

    if (typeof element === "object" && element != null) {
        loopThrowObjects(element, indexerObjects);
    } else {

        if (element === null) {
            if (propertyList.DOM) {
                propertyList.DOM.attr('data-empty', 'true');
            }
            propertyList.loopBreak = true;
        } else if (element) {
            propertyList.DOM.removeAttr('data-empty');
        }
    }
}

window.getEventTarget = function (element) {
    const tagName = element.prop('tagName').toLowerCase();
    const targetEvent = eventUpdateNames[tagName];
    return targetEvent;
}

window.propertyListReset = function (keys) {
    for (const key of keys) {
        window.propertyList[key] = null;
    }
}

window.triggerCloneInput = function (element) {
    const input = element.find('.input-json').last();

    if (!input.length) return;

    const targetEvent = getEventTarget(input);


    input.trigger(targetEvent);
}

window.triggerClone = function (element) {
    const index = 0;
    generateDataInputsClonable(loopThrowClonableGlobal(element, index, loopThrowInputsClonable));

    triggerCloneInput(element);
}

window.loopThrowInputsJsonClass = function (selector, parent, callback) {

    const inputs = parent ? parent.find(selector) : $(selector);

    for (let input of inputs) {
        input = $(input);
        callback(input);
    }
}

window.shamsiDatePickerOptions = {
    format: 'X',
    timePicker: {
        enabled: true
    },
    initialValue: false,
    onSelect: function () {
        const thisElement = $(this.model.inputElement);
        thisElement.trigger('input');
    }
}

window.select2AjaxOptions = {
    ajax: {
        url: "https://api.rapidcode.ir/rest/index.php/products",
        dataType: 'json',
        data: function (params) {

            const taxonomy = $(this).attr('data-taxonomy');

            const query = {
                q: params.term,
                taxonomy: taxonomy
            }

            return query;
        },
        processResults: function (data) {

            return {
                results: data,
            };
        },

        cache: true
    },
    placeholder: 'انتخاب کنید',
    allowClear: true,
    minimumInputLength: 1,
    language: "fa",
    templateResult: function (data) { },
    templateSelection: function (data) { }
};

window.jqueryClonerOptions = {
    clonableContainer: '.clonable-block',
    clonable: '.clonable',
    addButton: '.clonable-button-add',
    closeButton: '.clonable-button-close',
    focusableElement: ':input:visible:enabled:first',
    clearValueOnClone: true,
    removeNestedClonablesOnClone: true,
    limitCloneNumbers: true,
    debug: false,
    cloneName: 'clonable-clone',
    sourceName: 'clonable-source',
    clonableCloneNumberDecrement: 'clonable-clone-number-decrement',
    incrementName: 'clonable-increment',
    decrementName: 'clonable-decrement',

    beforeToggle: function (clone, index, self) { },
    afterToggle: function (clone, index, self) { },
    onDeleteRow: onDeleteRow
}

window.propertyList = {
    loopBreak: null,
    DOM: null
}

window.eventUpdateNames = {
    input: 'input',
    select: 'change'
}


// fix clone select2
$('.clipboardIT').each(function (index, element) {
    element = $(element);
    window[element.attr('data-clipboard-id')] = element.get(0).outerHTML
})

// generate persian date picker
generatePersianDatepickerFeature($('input.date-picker-shamsi'), shamsiDatePickerOptions)

// generate active clone
$('.activatation').on('change', cloneActivation)

// generate active clone
$('.clonable-button-close').on('click', onDeleteRow)

// generate number seperator
$('input[data-seperator]').on("input", onInputNumberSeperator)

// enable Select2AJAX options
const select2AjaxOptionLocal = select2AjaxOptions;
select2AjaxOptionLocal.templateResult = showSelect2AjaxResult;
select2AjaxOptionLocal.templateSelection = showSelection2AjaxResult;

// enable Select2AJAX Specification
generateSelect2Feature($('.specification .select2.select2ajax'), select2AjaxOptionLocal);

// enable Select2AJAX Variation
generateSelect2Feature($('.variation .select2.select2ajax'), select2AjaxOptionLocal);

// set specification clone options
const cloneOptions = jqueryClonerOptions;

cloneOptions.afterToggle = function (clone, index) {
    loopThrowClonableLocal(clone, index, callListedFunctions);
    generateUniqueIdDom(clone , ['.select2']);
    select2ClonerFixer(clone, index);
    persianDatePickerClonerFixer(clone, index);
    generateDataInputsClonable(loopThrowClonableGlobal(clone, index, loopThrowInputsClonable));
    triggerClone(clone);
};

// enable specification clone
$('#clone_specification').cloner(cloneOptions).on('change', select2Clear);
// trigger specification first clone
triggerClone($('#clone_specification .clonable').first());

// enable variation clone
$('#clone_variation').cloner(cloneOptions).on('change', '.select2', select2Clear);
// trigger variation first clone
triggerClone($('#clone_variation .clonable').first());

// enable variation clone depth 2
$('.variation #depth-2-1').cloner(cloneOptions);

})

دانلود سورس clone از html با جاوا اسکریپت


چگونه از المنت های html نمونه بگیریم

ارسال نظر

جهت استفاده از کد حتما از تگ pre استفاده نمایید .

contact us

انجام انواع پروژه های وب


( فروشگاهی ، خبری ، رزرواسیون ، وردپرس ، حل مشکلات وردپرسی )

شماره تماس و واتساپ : 09398554859